20.1 预处理的步骤
- 三连符(如用??= 表示#号)替换成相应的单字符。
- 用
\斜线续行的多行代码接成一行(规定这种续行写法要求斜线后面紧跟换行)。 - 把每个注释替换成一个空格。
- 上面两步去掉了一些换行,剩下的代码行称为逻辑代码行(一条预处理由一个逻辑代码行组成),预处理器把每个逻辑代码行划分成预处理 Token 和空白字符,这种 Token 包括:各种标识符、关键字、常量、字面值、运算符和其他标点符号。
- 在 Token 中识别出宏定义和预处理指示,如遇宏定义则做宏展开,如遇 #include 预处理指示则把相应源文件包含进来,并对该源文件做 1~5 预处理。
- 找出字符常量或字符串字面值中的转义序列,用相应字节来替换,如把
\n替换成字节0x0a。 - 把相邻字符串字面值连接起来。
- 经以上处理后,把还剩下的空白字符丢掉,把 Token 交给 C 编译器做语法解析,此时就不再是预处理 Token,而叫 C Token 了。
最初例如:
#define STR "hello, "\"world"printf(STR);
预处理后交给 C 编译器的 Token 如有数组表示就是 5 个长度的 Token 数组:
[printf, (, "hello, world", ), ;]
20.2 宏定义
i. 变量式宏定义
define STR “hello, world”
ii. 函数式宏定义
函数式宏定义和真正函数调用的区别:
- 函数式宏定义的参数没有类型。
- 宏定义每次调用编译生成的指令都相当于一个函数体,而不是传参指令和 call 指令,会增大编译后的目标文件的体积。
- 定义宏定义要小心展开后运算符优先级不对或语法不对。
- 调用真函数式先求实参表达式再传给形参,而宏定义可能有问题。
尽管函数式宏定义和真函数比有很多缺点,但合理使用会显著提高代码执行效率,省去了分配和释放栈帧、传参、传返回值等工作。
在宏展开重复生成指令与频繁调用函数的开销之间权衡之后,一般来说简短的、被频繁调用的函数适合用函数式宏定义来代替实现**。**
用 gcc -E 选项或 cpp 命令可以得到预处理后的结果。
#define MAX(a, b) ((a) > (b) ? (a) : (b))k = MAX(i&0x0f, j&0x0f)/* gcc -E 输出为:*/k = ((i&0x0f) > (j&0x0f) ? (i&0x0f) : (j&0x0f))
函数式宏定义经常写成这样,如内核代码 /usr/include/linux目录下的头文件:
do {...} whilte (0); 是一条语句,这样展开后如遇到 if 表达式等可避免一些语法错误。
iii. 内联函数
C99 引入新的关键字 inline,用于定义内联函数(Inline Function),该写法在内核中很常见。
和 C++ 中的 inline 一样,C99 的 inline 也是对编译器的一个提示,提示编译器尽量使用函数的内联定义,去除函数调用带来的开销。inline 只有在开启编译器优化选项时才会生效。
使用如下命令,可观察到没有生成调用 MAX 函数的 call 指令,其指令内联在 max 函数中:
gcc -g -O main.c
objdump -dS a.out
inline int MAX(int a, int b){return a > b ? a : b;}int a[] = { 9, 3, 5, 2, 1, 0, 8, 7, 6, 4 };int max(int n){return n == 0 ? a[0] : MAX(a[n], max(n-1));}int main(void){max(9);return 0;}
iv. #、##运算符和可变参数
#和##是两个预处理运算符。
#在函数式宏定义中,# 号后面应该跟一个形参,用于创建字符串字面值。另外,若实参中包含字符常量或字符串字面值时,宏展开后会做转换:
#define STR(s) #sSTR(hello world "test") /* "hello world \"test\"" */
宏定义(即包括变量式和函数式宏定义)中可以用 ## 运算符把前后两个预处理 Token 连接成一个预处理 Token:
#define CONCAT(a, b) a##b#define HASH_HASH # ## #CONCAT(con, cat) /* concat */HASH_HASH /* ## */
函数式宏定义也可以像 printf 函数一样带有可变参数,同样用 ... 表示可变参数:
#define showlist(...) printf(#__VA_ARGS__)#define report(test, ...) ((test) ? printf(#test) : printf(__VA_ARGS__))showlist(The first, second, and third items.);report(x > y, "x is %d but y is %d", x, y);/* 预编译后: */printf("The first, second, and third items.");((x > y) ? printf("x > y") : printf("x is %d but y is %d", x, y));
函数式宏定义允许传空参数:
定义时不带参数,调用时也不能带参数:#define FOO() fooFOO() /* foo */定义时带一个参数,调用时可以传一个参数,也可以不传:#define FOO(a) foo##aFOO(bar) /* foobar */FOO() /* foo */定义时带三个参数:#define FOO(a, b, c) a##b##cFOO(1, 2, 3) /* 123 */FOO(1, 2, ) /* 12 */FOO(1, , 3) /* 13 */FOO(, , 3) /* 3 */FOO(1,2) /* 报错,因为空参数也是个参数,而传参时没有给出空参数的坑位,参数数量和定义不匹配 */空参数结合可变参数:#define FOO(a, ...) a##__VA_ARGS__FOO(1) /* 1 可变参数部分传了空参数 */FOO(1, 2, 3, ) /* 12, 3, 可变参数部分传了三个参数,第三个参数是空,看起来比较傻瓜化的替换,连传参时的空格也原样输出了...*/
gcc 拓展语法,如果 ## 运算符用在 **__VA_ARGS**__ 前面,除了起连接 Token 作用外还有特殊用法:
#define DEBUGP(format, ...) printk(format, ##__VA_ARGS__)DEBUGP("info no. %d", 1) /* printk("info no. %d", 1) 可变参数部分传了 1 */DEBUGP("info") /* printk("info") */
第二个例子中照理说是 printk("info",),但当 **__VA_ARGS**__ 是空参数时,## 运算符把前面的逗号“吃”掉了。
v. #undef 预处理指示
C 规定一模一样的定义才算是真正的重复定义,而真正的重复定义存在时是会报 redefined 警告的,所以如果需要重新定义一个宏,可先用 #undef 取消原来的定义,再重新定义。
重复取消一个宏定义不算错,就算没定义,取消定义也不会报错,比较宽松:
#define X 3#undef X#define X 2X /* 2 */
vi. 宏展开的步骤
有些宏展开要做多次替换,除了带 # 和 ## 运算符的参数之外,其他参数在替换之前要(在函数体中)对实参本身做充分展开。
嵌套的函数式宏定义,先从最内层的入参开始展开:
#define x 3#define f(a) f(x * (a))#undef x#define x 2#define g f#define t(a) at(t(g)(0) + t)(1);/* 展开步骤 */// 1. t(t(f)(0) + t)(1);// 2. t(f(0) + t)(1);// 3. t(f(2 * (0)) + t)(1);// 4. f(2 * (0)) + t(1); /* 最终展开结果 */
20.3 条件预处理指示
关于用途,之前链接详解中见过 Header Guard 写法:
#ifndef HEADER_FILENAME#define HEADER_FILENAME/* body of header */#endif
在所有需要配置的源文件开头包含一个头文件,在该头文件中进行宏定义,从而只需改一个头文件就可以影响所有包含它的源文件。
例如,内核源代码的配置管理:
- make menuconfig 命令生成 .config 文件
- 编译内核时根据 .config 文件生成 include/linux/autoconf.h
- include/linux/autoconf.h 被另一个头文件 include/linux/config.h 所包含
- 通常内核代码包含后者,即包含 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:
#define VERSION 2#if defined x || y || VERSION < 3
把表达式像 C 表达式一样求值 0 || 0 || 2 < 3,最终 #if 1 条件成立。
使用 #if 0 和 #endif 可临时注释多行代码,避免了中间可能存在 /* ... */ 注释的问题,因为多行注释不能嵌套,会报错:
#if 0代码行代码行...#endif
20.4 其他预处理特性
#pragma 预处理指示供编译器实现扩展特性,如 gcc 的 #pragma GCC ...,C 标准没有规定 #pragma 后面应该写什么及起什么作用。
C 标准规定了几个特殊的宏,无需定义即可使用,这样的特殊宏定义由编译器内建,最常用的如 __FILE__ 和 __LINE__ :
__FILE__展开成当前源文件的文件名。__LINE__展开成当前代码行的行号,是一个整数。
实现 assert,理解这两个特殊宏定义,也是本章的综合运用。
C 标准规定 assert 应该实现成函数式宏定义而不是真正的函数,另外还规定 C 标准库的头文件是相互独立的,使用 gcc main.c xassert.c 进行编译:
/* assert.h standard header */#undef assert#ifdef NDEBUG#define assert(test) ((void)0) /* 直接定义成 void 类型的值,什么也不做 */#elsevoid _Assert(char *);#define _STR(x) _VAL(x)#define _VAL(x) #x/* 测试条件是否成立,条件成立等于什么都不做,不成立调用 _Assert 打印调用信息 *//* 假如直接使用 _VAL(x) #x 的话,x 可以是个宏,但 # 运算符只会创建字符串字面值,产出 "__LINE__" 不是我们想要的 */#define assert(test) ((test) ? (void)0 \: _Assert(__FILE__ ":" _STR(__LINE__) " " #test))#endif
fputs 函数向标准错误输出打印错误信息,abort 函数异常终止当前进程:
/* xassert.c _Assert function */#include <stdio.h>#include <stdlib.h>void _Assert(char *mesg){fputs(mesg, stderr);fputs(" -- assertion failed\n", stderr);abort();}
/* main.c */#include "assert.h"int main(void){assert(2 > 3);return 0;}
运行结果:
C99 引入特殊标识符 __func__,在打印调试信息时可打印出当前函数名,该标识符是一个变量而不是宏定义,不在预处理阶段求值:
#include <stdio.h>void myfunc(void){printf("%s\n", __func__);}int main(void){myfunc();printf("%s\n", __func__);return 0;}
运行结果:
