预备知识:

  • 头文件

C程序在编译前,要进行一个 预处理 操作,这个操作主要的工作是根据 预处理指令 对C源代码中的一些文本进行转换操作,生成另外的特定文本。

每一条预处理指令都由一个 # 开头,后接具体的指令内容。我们会介绍C语言中的各种 预处理指令 ,他们对于C程序至关重要,例如包含文件、定义常量、选择编译等等。

当然,在此之前,我们其实已经见过了不少的宏定义指令了,但是仅仅一带而过,并未详细展开,在这里会进行详细讲解。
不过,对于初学者不常用的指令在此从略。

包含其他文件:#include

我们已经无数次地看到 #include 指令了,这条预处理指令用于在当前文件中包含其他文件,被包含的文件内容会被直接以纯文本的形式替换该指令。

基本语法

  • #include <文件名>
  • #include "文件名"

使用 <> 时,从标准包含目录搜索指定的文件进行包含;
使用 "" 时,优先从当前文件所处目录进行搜索,未找到时再去标准包含目录搜索。

注意,以上两种操作实际上均由实现定义。

替换文本宏:#define和#undef

基本语法:

  • #define 标识符 替换列表
  • #define 标识符(形参) 替换列表
  • #define 标识符(形参...) 替换列表 (C99起)
  • #define 标识符(...) 替换列表 (C99起)
  • #undef 标识符

注:考虑到本文面向初学者,上述语法中C99新增的2种在此并不展开讲解,读者可以自行查阅相关资料。

详细解释:

#define 指令定义 标识符 ,即后面所有出现的该 标识符 均被替换为 形参列表,并且这个替换是 纯文本替换 ,并不涉及任何的算术运算。
注:标识符被定义为宏常称为“宏定义”,常称这种替换为 “宏替换”。

例如:

1
2
3
4
5
6
7
#include <stdio.h>
#define A_NUM 2333
int main() {
int i = A_NUM;
printf("i=%d",i);
return 0;
}

运行结果为:

1
i=2333

这里将 #define 指令后面所有的标识符 A_NUM 全部简单地替换为 形参列表 表示的文本“2333”,这样,源代码中对变量 i 的赋值就变成了 int i = 2333;

另一方面,需要注意语法中特别强调了 标识符 ,这意味着其必须符合前面所述的 标识符命名规则,否则程序报错;此外,这也意味着,如果在字符串中包含了该 标识符 文本,他会被视为字符串的一部分,并不会替换。
例如:

1
2
3
4
5
6
7
#include <stdio.h>
#define HELLO hello
int main() {
char str[] = "HELLO";
printf("str=%s",str);
return 0;
}

运行结果为:

1
str=HELLO

显然,此时并没有发生替换,也就是说 "HELLO" 被视为一个普通的字符串,不会进行宏替换。

以上这种简单替换标识符的宏被称为 仿对象宏


宏定义也可以类似函数那样使用形参,唯一的区别是,宏定义的形参没有类型,它依然是一个简单的文本替换。
例如,我们尝试使用宏来实现一个简单的求2者最大值的函数:

1
2
3
4
5
6
7
8
#include <stdio.h>
#define MAX(x,y) (x>y?x:y)
int main() {
int i=3,j=4;
int max_num = MAX(i,j);
printf("max_num = %d",max_num);
return 0;
}

运行结果为:

1
max_num = 4

而且由于形参x和y并没有类型限制,所以可以传入任何可以比大小的2个值:

1
2
3
4
5
6
7
8
#include <stdio.h>
#define MAX(x,y) (x>y?x:y)
int main() {
double i=3.14,j=12.5;
double max_num = MAX(i,j);
printf("max_num = %.2lf",max_num);
return 0;
}

运行结果为:

1
max_num = 12.50

像这样定义的函数称为 仿函数宏,当然,许多人将其称为 宏函数 ,并无伤大雅。


而对于 `#undef` 指令,用于解除前面的 `#define` 指令所定义的宏,若标识符无与之关联的宏,则忽略此指令。 例如下面的代码,在定义变量 j 的时候便会报错表示宏 `A_NUM` 未定义:
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#define A_NUM 2333
int main() {
int i = A_NUM + 1;
#undef A_NUM
int j = A_NUM + 2;
printf("i=%d,j = %d", i, j);
return 0;
}

特殊操作:#运算符与##运算符

宏替换是纯文本,自然让我们想到一些字符串操作,仿函数宏中的 ### 运算符实现了这一点。

  • # 运算符将标识符 字符串化,即将替换列表中的标识符加上双引号,如果标识符的值包含有 ",则会自动进行转义;
    此外,该运算符还会将所有的前导、尾随空格删除,并且将任何文本中间的空白字符序列(但非内嵌字符串字面量中的)缩减为一个空格。
    例如:
1
2
3
4
#define TO_STRING(x) #x
TO_STRING(3) // 展开成 "3"
TO_STRING("hello") // 展开成 "\"hello\""
TO_STRING( abc 323 ) // 展开成 "abc 323"
  • 在任意两个标识符中间的 ## 运算符将两者进行替换,然后将结果连接,若连接的结果不是合法记号,则行为未定义。
    例如:
1
2
#define ARRAY(var) var##_arr
char ARRAY(name)[10]; // 展开成 char name_arr[10];

注意事项:

  • 不要在宏中引发副作用,例如 #define SQUARE(x) ((x)=(x)*(x)) 并不合适。
  • 宏过长的话请在行末使用 \ 进行换行,以保证代码的整洁,注意最后一行不要加 \ 。例如:
1
2
3
4
5
6
7
8
9
10
#define BE_BYTES_TO_UINT64(b, u)                     \
    ((u) = (uint64_t)((b)[0]) << 56                  \
         | (uint64_t)((b)[1]) << 48                  \
         | (uint64_t)((b)[2]) << 40                  \
         | (uint64_t)((b)[3]) << 32                  \
         | (uint64_t)((b)[4]) << 24                  \
         | (uint64_t)((b)[5]) << 16                  \
         | (uint64_t)((b)[6]) <<  8                  \
         | (uint64_t)((b)[7])                        \
    )
  • 宏是可以嵌套的,对于嵌套宏,从外向内逐层展开。
  • 宏仅仅是在编译期进行简单的文本替换,而不会做任何的计算,所以请不要认为仿函数宏中传递的参数是先被计算求值然后再展开!展开后的结果仍然保留原来的表达式。例如:
1
2
3
#define SQUARE(x) x * x
int j = 3;
int k = SQUARE(j+1); // 展开后的结果是 SQUARE(j+1 * j+1) 而不是 SQUARE(4 * 4),因此会获得错误的结果 7
  • 针对上一条指出的问题,在仿函数宏中,请对整个替换列表中出现的标识符加括号,例如:
1
2
3
#define SQUARE(x) ((x) * (x))
int j = 3;
int k = SQUARE(j+1); // 展开后的结果是 SQUARE(((j+1) * (j+1))) 这样就能得到期望的正确结果 16

条件编译指令:#if、#ifdef等