20.1 预处理的步骤

  1. 三连符(如用??= 表示#号)替换成相应的单字符。
  2. \斜线续行的多行代码接成一行(规定这种续行写法要求斜线后面紧跟换行)。
  3. 把每个注释替换成一个空格。
  4. 上面两步去掉了一些换行,剩下的代码行称为逻辑代码行(一条预处理由一个逻辑代码行组成),预处理器把每个逻辑代码行划分成预处理 Token 和空白字符,这种 Token 包括:各种标识符、关键字、常量、字面值、运算符和其他标点符号。
  5. 在 Token 中识别出宏定义和预处理指示,如遇宏定义则做宏展开,如遇 #include 预处理指示则把相应源文件包含进来,并对该源文件做 1~5 预处理。
  6. 找出字符常量或字符串字面值中的转义序列,用相应字节来替换,如把 \n 替换成字节 0x0a
  7. 把相邻字符串字面值连接起来。
  8. 经以上处理后,把还剩下的空白字符丢掉,把 Token 交给 C 编译器做语法解析,此时就不再是预处理 Token,而叫 C Token 了。

最初例如:

  1. #define STR "hello, "\
  2. "world"
  3. printf(
  4. STR);

预处理后交给 C 编译器的 Token 如有数组表示就是 5 个长度的 Token 数组:

  1. [printf, (, "hello, world", ), ;]

20.2 宏定义

较大的项目会用大量的宏定义来组织代码。

i. 变量式宏定义

define STR “hello, world”

ii. 函数式宏定义

函数式宏定义和真正函数调用的区别:

  1. 函数式宏定义的参数没有类型。
  2. 宏定义每次调用编译生成的指令都相当于一个函数体,而不是传参指令和 call 指令,会增大编译后的目标文件的体积。
  3. 定义宏定义要小心展开后运算符优先级不对或语法不对。
  4. 调用真函数式先求实参表达式再传给形参,而宏定义可能有问题。

尽管函数式宏定义和真函数比有很多缺点,但合理使用会显著提高代码执行效率,省去了分配和释放栈帧、传参、传返回值等工作。
在宏展开重复生成指令与频繁调用函数的开销之间权衡之后,一般来说简短的、被频繁调用的函数适合用函数式宏定义来代替实现**。**

用 gcc -E 选项或 cpp 命令可以得到预处理后的结果。

  1. #define MAX(a, b) ((a) > (b) ? (a) : (b))
  2. k = MAX(i&0x0f, j&0x0f)
  3. /* gcc -E 输出为:*/
  4. k = ((i&0x0f) > (j&0x0f) ? (i&0x0f) : (j&0x0f))

函数式宏定义经常写成这样,如内核代码 /usr/include/linux目录下的头文件:
image.png
do {...} whilte (0); 是一条语句,这样展开后如遇到 if 表达式等可避免一些语法错误。

iii. 内联函数

C99 引入新的关键字 inline,用于定义内联函数(Inline Function),该写法在内核中很常见。
image.png
和 C++ 中的 inline 一样,C99 的 inline 也是对编译器的一个提示,提示编译器尽量使用函数的内联定义,去除函数调用带来的开销。inline 只有在开启编译器优化选项时才会生效。
使用如下命令,可观察到没有生成调用 MAX 函数的 call 指令,其指令内联在 max 函数中:
gcc -g -O main.c
objdump -dS a.out

  1. inline int MAX(int a, int b)
  2. {
  3. return a > b ? a : b;
  4. }
  5. int a[] = { 9, 3, 5, 2, 1, 0, 8, 7, 6, 4 };
  6. int max(int n)
  7. {
  8. return n == 0 ? a[0] : MAX(a[n], max(n-1));
  9. }
  10. int main(void)
  11. {
  12. max(9);
  13. return 0;
  14. }

iv. #、##运算符和可变参数

###是两个预处理运算符。

#在函数式宏定义中,# 号后面应该跟一个形参,用于创建字符串字面值。另外,若实参中包含字符常量或字符串字面值时,宏展开后会做转换:

  1. #define STR(s) #s
  2. STR(hello world "test") /* "hello world \"test\"" */

宏定义(即包括变量式和函数式宏定义)中可以用 ## 运算符把前后两个预处理 Token 连接成一个预处理 Token:

  1. #define CONCAT(a, b) a##b
  2. #define HASH_HASH # ## #
  3. CONCAT(con, cat) /* concat */
  4. HASH_HASH /* ## */

函数式宏定义也可以像 printf 函数一样带有可变参数,同样用 ... 表示可变参数:

  1. #define showlist(...) printf(#__VA_ARGS__)
  2. #define report(test, ...) ((test) ? printf(#test) : printf(__VA_ARGS__))
  3. showlist(The first, second, and third items.);
  4. report(x > y, "x is %d but y is %d", x, y);
  5. /* 预编译后: */
  6. printf("The first, second, and third items.");
  7. ((x > y) ? printf("x > y") : printf("x is %d but y is %d", x, y));

函数式宏定义允许传空参数:

  1. 定义时不带参数,调用时也不能带参数:
  2. #define FOO() foo
  3. FOO() /* foo */
  4. 定义时带一个参数,调用时可以传一个参数,也可以不传:
  5. #define FOO(a) foo##a
  6. FOO(bar) /* foobar */
  7. FOO() /* foo */
  8. 定义时带三个参数:
  9. #define FOO(a, b, c) a##b##c
  10. FOO(1, 2, 3) /* 123 */
  11. FOO(1, 2, ) /* 12 */
  12. FOO(1, , 3) /* 13 */
  13. FOO(, , 3) /* 3 */
  14. FOO(1,2) /* 报错,因为空参数也是个参数,而传参时没有给出空参数的坑位,参数数量和定义不匹配 */
  15. 空参数结合可变参数:
  16. #define FOO(a, ...) a##__VA_ARGS__
  17. FOO(1) /* 1 可变参数部分传了空参数 */
  18. FOO(1, 2, 3, ) /* 12, 3, 可变参数部分传了三个参数,第三个参数是空,看起来比较傻瓜化的替换,连传参时的空格也原样输出了...*/

gcc 拓展语法,如果 ## 运算符用在 **__VA_ARGS**__ 前面,除了起连接 Token 作用外还有特殊用法:

  1. #define DEBUGP(format, ...) printk(format, ##__VA_ARGS__)
  2. DEBUGP("info no. %d", 1) /* printk("info no. %d", 1) 可变参数部分传了 1 */
  3. DEBUGP("info") /* printk("info") */

第二个例子中照理说是 printk("info",),但当 **__VA_ARGS**__ 是空参数时,## 运算符把前面的逗号“吃”掉了。

v. #undef 预处理指示

C 规定一模一样的定义才算是真正的重复定义,而真正的重复定义存在时是会报 redefined 警告的,所以如果需要重新定义一个宏,可先用 #undef 取消原来的定义,再重新定义。
重复取消一个宏定义不算错,就算没定义,取消定义也不会报错,比较宽松:

  1. #define X 3
  2. #undef X
  3. #define X 2
  4. X /* 2 */

vi. 宏展开的步骤

有些宏展开要做多次替换,除了带 # 和 ## 运算符的参数之外,其他参数在替换之前要(在函数体中)对实参本身做充分展开。
嵌套的函数式宏定义,先从最内层的入参开始展开:

  1. #define x 3
  2. #define f(a) f(x * (a))
  3. #undef x
  4. #define x 2
  5. #define g f
  6. #define t(a) a
  7. t(t(g)(0) + t)(1);
  8. /* 展开步骤 */
  9. // 1. t(t(f)(0) + t)(1);
  10. // 2. t(f(0) + t)(1);
  11. // 3. t(f(2 * (0)) + t)(1);
  12. // 4. f(2 * (0)) + t(1); /* 最终展开结果 */

20.3 条件预处理指示

关于用途,之前链接详解中见过 Header Guard 写法:

  1. #ifndef HEADER_FILENAME
  2. #define HEADER_FILENAME
  3. /* body of header */
  4. #endif

在所有需要配置的源文件开头包含一个头文件,在该头文件中进行宏定义,从而只需改一个头文件就可以影响所有包含它的源文件。
例如,内核源代码的配置管理:

  1. make menuconfig 命令生成 .config 文件
  2. 编译内核时根据 .config 文件生成 include/linux/autoconf.h
  3. include/linux/autoconf.h 被另一个头文件 include/linux/config.h 所包含
  4. 通常内核代码包含后者,即包含 include/linux/config.h

内核获取地址:https://mirrors.edge.kernel.org/pub/linux/kernel/

ifdef 或 #if 可以嵌套使用,但预处理指示通常顶头写不缩进,为区分嵌套,一般在 #endif 处加注释。

#define NDEBUG 这样的宏定义,预处理时不是为了做替换,而是为了配合 #ifdef 等预处理指示测试是否定义过,所以定义成什么值都行,空就足够。
或用 gcc -D 选项定义宏,如 gcc -DNDEBUG 等同于上面写法。
第2种写法目前更适合,因为 “只写一次到处生效”,但等学了 Makefile 后,第三种也有办法只写一次到处生效。
NDEBUG 效果是可以禁用 assert.h 中的 assert 宏定义,书中第 11 章 p146 页演示过 assert。

if 后面的表达式必须是常量表达式,表达式中的标识符也只能是宏定义。
预处理运算符 defined 一般用作表达式中的一部分。单独使用时,#if defined x 相当于 #ifdef x#if !defined x 相当于 #ifndef x
而下例中的用法,x 未定义,则 defined x 替换成 0,否则替换成 1:

  1. #define VERSION 2
  2. #if defined x || y || VERSION < 3

把表达式像 C 表达式一样求值 0 || 0 || 2 < 3,最终 #if 1 条件成立。

使用 #if 0#endif 可临时注释多行代码,避免了中间可能存在 /* ... */ 注释的问题,因为多行注释不能嵌套,会报错:

  1. #if 0
  2. 代码行
  3. 代码行
  4. ...
  5. #endif

20.4 其他预处理特性

#pragma 预处理指示供编译器实现扩展特性,如 gcc 的 #pragma GCC ...,C 标准没有规定 #pragma 后面应该写什么及起什么作用。

C 标准规定了几个特殊的宏,无需定义即可使用,这样的特殊宏定义由编译器内建,最常用的如 __FILE____LINE__

  • __FILE__ 展开成当前源文件的文件名。
  • __LINE__ 展开成当前代码行的行号,是一个整数。

实现 assert,理解这两个特殊宏定义,也是本章的综合运用。
C 标准规定 assert 应该实现成函数式宏定义而不是真正的函数,另外还规定 C 标准库的头文件是相互独立的,使用 gcc main.c xassert.c 进行编译:

  1. /* assert.h standard header */
  2. #undef assert
  3. #ifdef NDEBUG
  4. #define assert(test) ((void)0) /* 直接定义成 void 类型的值,什么也不做 */
  5. #else
  6. void _Assert(char *);
  7. #define _STR(x) _VAL(x)
  8. #define _VAL(x) #x
  9. /* 测试条件是否成立,条件成立等于什么都不做,不成立调用 _Assert 打印调用信息 */
  10. /* 假如直接使用 _VAL(x) #x 的话,x 可以是个宏,但 # 运算符只会创建字符串字面值,产出 "__LINE__" 不是我们想要的 */
  11. #define assert(test) ((test) ? (void)0 \
  12. : _Assert(__FILE__ ":" _STR(__LINE__) " " #test))
  13. #endif

fputs 函数向标准错误输出打印错误信息,abort 函数异常终止当前进程:

  1. /* xassert.c _Assert function */
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. void _Assert(char *mesg)
  5. {
  6. fputs(mesg, stderr);
  7. fputs(" -- assertion failed\n", stderr);
  8. abort();
  9. }
  1. /* main.c */
  2. #include "assert.h"
  3. int main(void)
  4. {
  5. assert(2 > 3);
  6. return 0;
  7. }

运行结果:
image.png

C99 引入特殊标识符 __func__,在打印调试信息时可打印出当前函数名,该标识符是一个变量而不是宏定义,不在预处理阶段求值:

  1. #include <stdio.h>
  2. void myfunc(void)
  3. {
  4. printf("%s\n", __func__);
  5. }
  6. int main(void)
  7. {
  8. myfunc();
  9. printf("%s\n", __func__);
  10. return 0;
  11. }

运行结果:
image.png