第 21 章 预处理

1. 预处理的步骤

现在我们全面了解一下 C 编译器做语法解析之前的预处理步骤:

  1. 第 2 章「常量、变量和表达式」第 2 节「常量」提到过的三连符替换成相应的单字符。

  2. 把用\字符续行的多行代码接成一行。例如:

    #define STR "hello, "\
    		"world"
    

    经过这个预处理步骤之后接成一行:

    #define STR "hello, "		"world"
    

    这种续行的写法要求\后面紧跟换行,中间不能有其它空白字符。

  3. 把注释(不管是单行注释还是多行注释)都替换成一个空格。

  4. 经过以上两步之后去掉了一些换行,有的换行在续行过程中去掉了,有的换行在多行注释之中,也随着注释一起去掉了,剩下的代码行称为逻辑代码行。然后预处理器把逻辑代码行划分成 Token和空白字符,这时的 Token 称为预处理 Token,包括标识符、整数常量、浮点数常量、字符常量、字符串、运算符和其它符号。继续上面的例子,两个源代码行被接成一个逻辑代码行,然后这个逻辑代码行被划分成 Token 和空白字符:#define,空格,STR,空格,"hello, ",Tab,Tab,"world"

  5. 在 Token 中识别出预处理指示,做相应的预处理动作,如果遇到 #include 预处理指示,则把相应的源文件包含进来,并对源文件做以上 1 - 4 步预处理。如果遇到宏定义则做宏展开。 我们早在第 8 章「数组」第 2 节「数组应用实例:统计随机数」就认识了预处理指示这个概念,现在给出它的严格定义。一条预处理指示由一个逻辑代码行组成,以 # 开头,后面跟若干个预处理 Token,在预处理指示中允许使用的空白字符只有空格和 Tab。

  6. 找出字符常量或字符串中的转义序列,用相应的字节来替换它,比如把 \n 替换成字节 0x0a

  7. 把相邻的字符串连接起来。继续上面的例子,如果代码中有:

    printf(
    	STR);
    

    经过第 4 步处理划分成以下 Token:printf(,换行,Tab,STR);,换行。经过第 5 步宏展开后变成以下 Token:printf(,换行,Tab,"hello, ",Tab,Tab,"world");,换行。然后把相邻的字符串连接起来,变成以下 Token:printf(,换行,Tab,"hello, world");,换行。

  8. 经过以上处理之后,把空白字符丢掉,把 Token 交给 C 编译器做语法解析,这时就不再是预处理 Token,而称为 C Token了。这里丢掉的空白字符包括空格、换行、水平 Tab、垂直 Tab、分页符。继续上面的例子,最后交给 C 编译器做语法解析的 Token 是:printf("hello, world");。注意,把一个预处理指示写成多行要用 \ 续行,因为根据定义,一条预处理指示只能由一个逻辑代码行组成,而把 C 代码写成多行则不需要用 \ 续行,因为换行在 C 代码中只不过是一种空白字符,在做语法解析时所有空白字符都已经丢掉了。

2. 宏定义

较大的项目都会用大量的宏定义来组织代码,你可以看看 /usr/include 下面的头文件中用了多少个宏定义。看起来宏展开就是做个替换而已,其实里面有比较复杂的规则,C 语言有很多复杂但不常用的语法规则本书并不涉及,但有关宏展开的语法规则本节却力图做全面讲解,因为它很重要也很常用。

2.1. 函数式宏定义

以前我们用过的 #define N 20#define STR "hello, world" 这种宏定义可以称为变量式宏定义(Object-like Macro),宏定义名可以像变量一样在代码中使用。另外一种宏定义可以像函数调用一样在代码中使用,称为函数式宏定义(Function-like Macro)。例如编辑一个文件 main.c

#define MAX(a, b) ((a)>(b)?(a):(b))
k = MAX(i&0x0f, j&0x0f)

我们想看第二行的表达式展开成什么样,可以用 gcc-E 选项或 cpp 命令,尽管这个 C 程序不合语法,但没关系,我们只做预处理而不编译,不会检查程序是否符合 C 语法。

$ cpp main.c
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"

k = ((i&0x0f)>(j&0x0f)?(i&0x0f):(j&0x0f))

就像函数调用一样,把两个实参分别替换到宏定义中形参 ab 的位置。注意这种函数式宏定义和真正的函数调用有什么不同:

  1. 函数式宏定义的参数没有类型,预处理器只负责做形式上的替换,而不做参数类型检查,所以传参时要格外小心。
  2. 调用真正函数的代码和调用函数式宏定义的代码编译生成的指令不同。如果 MAX 是个真正的函数,那么它的函数体 return a > b ? a : b; 要编译生成指令,代码中出现的每次调用也要编译生成传参指令和 call 指令。而如果 MAX 是个函数式宏定义,这个宏定义本身倒不必编译生成指令,但是代码中出现的每次调用编译生成的指令都相当于一个函数体,而不是简单的几条传参指令和 call 指令。所以,使用函数式宏定义编译生成的目标文件会比较大。
  3. 定义这种宏要格外小心,如果上面的定义写成 #define MAX(a, b) (a>b?a:b),省去内层括号,则宏展开就成了 k = (i&0x0f>j&0x0f?i&0x0f:j&0x0f),运算的优先级就错了。同样道理,这个宏定义的外层括号也是不能省的,想一想为什么。
  4. 调用函数时先求实参表达式的值再传给形参,如果实参表达式有 Side Effect,那么这些 Side Effect 只发生一次。例如 MAX(++a, ++b),如果 MAX 是个真正的函数,ab 只增加一次。但如果 MAX 是上面那样的宏定义,则要展开成 k = ((++a)>(++b)?(++a):(++b))ab 就不一定是增加一次还是两次了。
  5. 即使实参没有 Side Effect,使用函数式宏定义也往往会导致较低的代码执行效率。下面举一个极端的例子,也是个很有意思的例子。

例 21.1. 函数式宏定义

#define MAX(a, b) ((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;
}

这段代码从一个数组中找出最大的数,如果 MAX 是个真正的函数,这个算法就是从前到后遍历一遍数组,时间复杂度是 Θ(n),而现在 MAX 是这样一个函数式宏定义,思考一下这个算法的时间复杂度是多少?

尽管函数式宏定义和真正的函数相比有很多缺点,但只要小心使用还是会显著提高代码的执行效率,毕竟省去了分配和释放栈帧、传参、传返回值等一系列工作,因此那些简短并且被频繁调用的函数经常用函数式宏定义来代替实现。例如 C 标准库的很多函数都提供两种实现,一种是真正的函数实现,一种是宏定义实现,这一点以后还要详细解释。

函数式宏定义经常写成这样的形式(取自内核代码 include/linux/pm.h):

#define device_init_wakeup(dev,val) \
        do { \
                device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val); \
        } while(0)

为什么要用 do { ... } while(0) 括起来呢?不括起来会有什么问题呢?

#define device_init_wakeup(dev,val) \
                device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val);

if (n > 0)
	device_init_wakeup(d, v);

这样宏展开之后,函数体的第二条语句不在 if 条件中。那么简单地用 { ... } 括起来组成一个语句块不行吗?

#define device_init_wakeup(dev,val) \
                { device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val); }

if (n > 0)
	device_init_wakeup(d, v);
else
	continue;

问题出在 device_init_wakeup(d, v); 末尾的 ; 号,如果不允许写这个 ; 号,看起来不像个函数调用,可如果写了这个 ; 号,宏展开之后就有语法错误,if 语句被这个 ; 号结束掉了,没法跟 else 配对。因此,do { ... } while(0) 是一种比较好的解决办法。

如果在一个程序文件中重复定义一个宏,C 语言规定这些重复的宏定义必须一模一样。例如这样的重复定义是允许的:

#define OBJ_LIKE (1 - 1)
#define OBJ_LIKE /* comment */ (1/* comment */-/* comment */  1)/* comment */

在定义的前后多些空白(空格、Tab、注释)没有关系,在定义之中多些空白或少些空白也没有关系,但在定义之中有空白和没有空白被认为是不同的,所以这样的重复定义是不允许的:

#define OBJ_LIKE (1 - 1)
#define OBJ_LIKE (1-1)

如果需要重新定义一个宏,和原来的定义不同,可以先用 #undef 取消原来的定义,再重新定义,例如:

#define X 3
... /* X is 3 */
#undef X
... /* X has no definition */
#define X 2
... /* X is 2 */

2.2. 内联函数

C99 引入一个新关键字 inline,用于定义内联函数(inline function)。这种用法在内核代码中很常见,例如 include/linux/rwsem.h中:

static inline void down_read(struct rw_semaphore *sem)
{
        might_sleep();
        rwsemtrace(sem,"Entering down_read");
        __down_read(sem);
        rwsemtrace(sem,"Leaving down_read");
}

inline 关键字告诉编译器,这个函数的调用要尽可能快,可以当普通的函数调用实现,也可以用宏展开的办法实现。我们做个实验,把上一节的例子改一下:

例 21.2. 内联函数

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;
}

按往常的步骤编译然后反汇编:

$ gcc main.c -g
$ objdump -dS a.out
...
int max(int n)
{
 8048369:       55                      push   %ebp
 804836a:       89 e5                   mov    %esp,%ebp
 804836c:       83 ec 0c                sub    $0xc,%esp
        return n == 0 ? a[0] : MAX(a[n], max(n-1));
 804836f:       83 7d 08 00             cmpl   $0x0,0x8(%ebp)
 8048373:       75 0a                   jne    804837f <max+0x16>
 8048375:       a1 c0 95 04 08          mov    0x80495c0,%eax
 804837a:       89 45 fc                mov    %eax,-0x4(%ebp)
 804837d:       eb 29                   jmp    80483a8 <max+0x3f>
 804837f:       8b 45 08                mov    0x8(%ebp),%eax
 8048382:       83 e8 01                sub    $0x1,%eax
 8048385:       89 04 24                mov    %eax,(%esp)
 8048388:       e8 dc ff ff ff          call   8048369 <max>
 804838d:       89 c2                   mov    %eax,%edx
 804838f:       8b 45 08                mov    0x8(%ebp),%eax
 8048392:       8b 04 85 c0 95 04 08    mov    0x80495c0(,%eax,4),%eax
 8048399:       89 54 24 04             mov    %edx,0x4(%esp)
 804839d:       89 04 24                mov    %eax,(%esp)
 80483a0:       e8 9f ff ff ff          call   8048344 <MAX>
 80483a5:       89 45 fc                mov    %eax,-0x4(%ebp)
 80483a8:       8b 45 fc                mov    -0x4(%ebp),%eax
}
...

可以看到 MAX 是作为普通函数调用的。如果指定优化选项编译,然后反汇编:

$ gcc main.c -g -O
$ objdump -dS a.out
...
int max(int n)
{
 8048355:       55                      push   %ebp
 8048356:       89 e5                   mov    %esp,%ebp
 8048358:       53                      push   %ebx
 8048359:       83 ec 04                sub    $0x4,%esp
 804835c:       8b 5d 08                mov    0x8(%ebp),%ebx
        return n == 0 ? a[0] : MAX(a[n], max(n-1));
 804835f:       85 db                   test   %ebx,%ebx
 8048361:       75 07                   jne    804836a <max+0x15>
 8048363:       a1 a0 95 04 08          mov    0x80495a0,%eax
 8048368:       eb 18                   jmp    8048382 <max+0x2d>
 804836a:       8d 43 ff                lea    -0x1(%ebx),%eax
 804836d:       89 04 24                mov    %eax,(%esp)
 8048370:       e8 e0 ff ff ff          call   8048355 <max>
inline int MAX(int a, int b)
{
        return a > b ? a : b;
 8048375:       8b 14 9d a0 95 04 08    mov    0x80495a0(,%ebx,4),%edx
 804837c:       39 d0                   cmp    %edx,%eax
 804837e:       7d 02                   jge    8048382 <max+0x2d>
 8048380:       89 d0                   mov    %edx,%eax
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));
}
 8048382:       83 c4 04                add    $0x4,%esp
 8048385:       5b                      pop    %ebx
 8048386:       5d                      pop    %ebp
 8048387:       c3                      ret    
...

可以看到,并没有 call 指令调用 MAX 函数,MAX 函数的指令是内联在 max 函数中的,由于源代码和指令的次序无法对应,maxMAX 函数的源代码也交错在一起显示。

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

在函数式宏定义中,# 运算符用于创建字符串,# 运算符后面应该跟一个形参(中间可以有空格或 Tab),例如:

#define STR(s) # s
STR(hello 	world)

cpp 命令预处理之后是 "hello␣world",自动用 " 号把实参括起来成为一个字符串,并且实参中的连续多个空白字符被替换成一个空格。

再比如:

#define STR(s) #s
fputs(STR(strncmp("ab\"c\0d", "abc", '\4"')
	== 0) STR(: @\n), s);

预处理之后是 fputs("strncmp(\"ab\\\"c\\0d\", \"abc\", '\\4\"') == 0" ": @\n", s);,注意如果实参中包含字符常量或字符串,则宏展开之后字符串的界定符 " 要替换成 \",字符常量或字符串中的 \" 字符要替换成 \\\"

在宏定义中可以用 ## 运算符把前后两个预处理 Token 连接成一个预处理 Token,和 # 运算符不同,## 运算符不仅限于函数式宏定义,变量式宏定义也可以用。例如:

#define CONCAT(a, b) a##b
CONCAT(con, cat)

预处理之后是 concat。再比如,要定义一个宏展开成两个 # 号,可以这样定义:

#define HASH_HASH # ## #

中间的 ## 是运算符,宏展开时前后两个 # 号被这个运算符连接在一起。注意中间的两个空格是不可少的,如果写成 ####,会被划分成 #### 两个 Token,而根据定义 ## 运算符用于连接前后两个预处理 Token,不能出现在宏定义的开头或末尾,所以会报错。

我们知道 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));

在宏定义中,可变参数的部分用 __VA_ARGS__ 表示,实参中对应 ... 的几个参数可以看成一个参数替换到宏定义中 __VA_ARGS__ 所在的地方。

调用函数式宏定义允许传空参数,这一点和函数调用不同,通过下面几个例子理解空参数的用法。

#define FOO() foo
FOO()

预处理之后变成 fooFOO 在定义时不带参数,在调用时也不允许传参数给它。

#define FOO(a) foo##a
FOO(bar)
FOO()

预处理之后变成:

foobar
foo

FOO 在定义时带一个参数,在调用时必须传一个参数给它,如果不传参数则表示传了一个空参数。

#define FOO(a, b, c) a##b##c
FOO(1,2,3)
FOO(1,2,)
FOO(1,,3)
FOO(,,3)

预处理之后变成:

123
12
13
3

FOO 在定义时带三个参数,在调用时也必须传三个参数给它,空参数的位置可以空着,但必须给够三个参数,FOO(1,2) 这样的调用是错误的。

#define FOO(a, ...) a##__VA_ARGS__
FOO(1)
FOO(1,2,3,)

预处理之后变成:

1
12,3,

FOO(1) 这个调用相当于可变参数部分传了一个空参数,FOO(1,2,3,) 这个调用相当于可变参数部分传了三个参数,第三个是空参数。

gcc 有一种扩展语法,如果 ## 运算符用在 __VA_ARGS__ 前面,除了起连接作用之外还有特殊的含义,例如内核代码 net/netfilter/nf_conntrack_proto_sctp.c 中的:

#define DEBUGP(format, ...) printk(format, ## __VA_ARGS__)

printk 这个内核函数相当于 printf,也带有格式化字符串和可变参数,由于内核不能调用 libc 的函数,所以另外实现了一个打印函数。这个函数式宏定义可以这样调用:DEBUGP("info no. %d", 1)。也可以这样调用:DEBUGP("info")。后者相当于可变参数部分传了一个空参数,但展开后并不是 printk("info",),而是 printk("info"),当 __VA_ARGS 是空参数时,## 运算符把它前面的 , 号「吃」掉了。

2.4. 宏展开的步骤

以上举的宏展开的例子都是最简单的,有些宏展开的过程要做多次替换,例如:

#define sh(x) printf("n" #x "=%d, or %d\n",n##x,alt[x])
#define sub_z  26
sh(sub_z)

sh(sub_z) 要用 sh(x) 这个宏定义来展开,形参 x 对应的实参是 sub_z,替换过程如下:

  1. #x 要替换成 "sub_z"
  2. n##x 要替换成 nsub_z
  3. 除了带 ### 运算符的参数之外,其它参数在替换之前要对实参本身做充分的展开,所以应该先把 sub_z 展开成 26 再替换到 alt[x]x 的位置。
  4. 现在展开成了 printf("n" "sub_z" "=%d, or %d\n",nsub_z,alt[26]),所有参数都替换完了,这时编译器会再扫描一遍,再找出可以展开的宏定义来展开,假设 nsub_zalt 是变量式宏定义,这时会进一步展开。

再举一个例子:

#define x 3
#define f(a) f(x * (a))
#undef x
#define x 2
#define g f
#define t(a) a

t(t(g)(0) + t)(1);

展开的步骤是:

  1. 先把 g 展开成 f 再替换到 #define t(a) a 中,得到 t(f(0) + t)(1);
  2. 根据 #define f(a) f(x * (a)),得到 t(f(x * (0)) + t)(1);
  3. x 替换成 2,得到 t(f(2 * (0)) + t)(1);。注意,一开始定义 x 为 3,但是后来用 #undef x 取消了 x 的定义,又重新定义 x 为2。当处理到 t(t(g)(0) + t)(1); 这一行代码时 x 已经定义成 2 了,所以用 2 来替换。还要注意一点,现在得到的 t(f(2 * (0)) + t)(1); 中仍然有 f,但不能再次根据 #define f(a) f(x * (a)) 展开了,f(2 * (0)) 就是由展开 f(0) 得到的,这里面再遇到 f 就不展开了,这样规定可以避免无穷展开(类似于无穷递归),因此我们可以放心地使用递归定义,例如 #define a a[0]#define a a.member 等。
  4. 根据 #define t(a) a,最终展开成 f(2 * (0)) + t(1);。这时不能再展开 t(1) 了,因为这里的 t 就是由展开 t(f(2 * (0)) + t) 得到的,所以不能再展开了。

3. 条件预处理指示

我们在上章第 2.2 节「头文件」中见过 Header Guard 的用法:

#ifndef HEADER_FILENAME
#define HEADER_FILENAME
/* body of header */
#endif

条件预处理指示也常用于源代码的配置管理,例如:

#if MACHINE == 68000
    int x;
#elif MACHINE == 8086
    long x;
#else    /* all others */
    #error UNKNOWN TARGET MACHINE
#endif

假设这段程序是为多种平台编写的,在 68000 平台上需要定义 xint 型,在 8086 平台上需要定义 xlong 型,对其它平台暂不提供支持,就可以用条件预处理指示来写。如果在预处理这段代码之前,MACHINE 被定义为 68000,则包含 intx; 这段代码;否则如果 MACHINE 被定义为 8086,则包含 long x; 这段代码;否则(MACHINE 没有定义,或者定义为其它值),包含 #error UNKNOWN TARGET MACHINE 这段代码,编译器遇到这个预处理指示就报错退出,错误信息就是 UNKNOWN TARGET MACHINE

如果要为 8086 平台编译这段代码,有几种可选的办法:

  1. 手动编辑代码,在前面添一行 #define MACHINE 8086。这样做的缺点是难以管理,如果这个项目中有很多源文件都需要定义 MACHINE,每次要为 8086 平台编译就得把这些定义全部改成 8086,每次要为 68000 平台编译就得把这些定义全部改成 68000。

  2. 在所有需要配置的源文件开头包含一个头文件,在头文件中定义 #define MACHINE 8086,这样只需要改一个头文件就可以影响所有包含它的源文件。通常这个头文件由配置工具生成,比如在 Linux 内核源代码的目录下运行 make menuconfig 命令可以出来一个配置菜单,在其中配置的选项会自动转换成头文件 include/linux/autoconf.h 中的宏定义。

    举一个具体的例子,在内核配置菜单中用回车键和方向键进入 Device Drivers ---> Network device support,然后用空格键选中 Network device support(菜单项左边的 [ ] 括号内会出现一个 * 号),然后保存退出,会生成一个名为 .config 的隐藏文件,其内容类似于:

    ...
    #
    # Network device support
    #
    CONFIG_NETDEVICES=y
    # CONFIG_DUMMY is not set
    # CONFIG_BONDING is not set
    # CONFIG_EQUALIZER is not set
    # CONFIG_TUN is not set
    ...
    

    然后运行 make 命令编译内核,这时根据 .config 文件生成头文件 include/linux/autoconf.h,其内容类似于:

    ...
    /*
     * Network device support
     */
    #define CONFIG_NETDEVICES 1
    #undef CONFIG_DUMMY
    #undef CONFIG_BONDING
    #undef CONFIG_EQUALIZER
    #undef CONFIG_TUN
    ...
    

    上面的代码用 #undef 确保取消一些宏的定义,如果先前没有定义过 CONFIG_DUMMY,用 #undef CONFIG_DUMMY 取消它的定义没有任何作用,也不算错。

    include/linux/autoconf.h 被另一个头文件 include/linux/config.h 所包含,通常内核代码包含后一个头文件,例如 net/core/sock.c

    ...
    #include <linux/config.h>
    ...
    int sock_setsockopt(struct socket *sock, int level, int optname,
                        char __user *optval, int optlen)
    {
    ...
    #ifdef CONFIG_NETDEVICES
                    case SO_BINDTODEVICE:
                    {
    			...
                    }
    #endif
    ...
    

    再比如drivers/isdn/i4l/isdn_common.c

    ...
    #include <linux/config.h>
    ...
    static int
    isdn_ioctl(struct inode *inode, struct file *file, uint cmd, ulong arg)
    {
    ...
    #ifdef CONFIG_NETDEVICES
                            case IIOCNETGPN:
                                    /* Get peer phone number of a connected
                                     * isdn network interface */
                                    if (arg) {
                                            if (copy_from_user(&phone, argp, sizeof(phone)))
                                                    return -EFAULT;
                                            return isdn_net_getpeer(&phone, argp);
                                    } else
                                            return -EINVAL;
    #endif
    ...
    #ifdef CONFIG_NETDEVICES
                            case IIOCNETAIF:
    ...
    #endif                          /* CONFIG_NETDEVICES */
    ...
    

    这样,在配置菜单中所做的配置通过条件预处理最终决定了哪些代码被编译到内核中。#ifdef#if 可以嵌套使用,但预处理指示通常都顶头写不缩进,为了区分嵌套的层次,可以像上面的代码中最后一行那样,在 #endif 处用注释写清楚它结束的是哪个 #if#ifdef

  3. 要定义一个宏不一定非得在代码中用 #define 定义,早在第 11 章「排序与查找」第 6 节「折半查找」我们就见过用 gcc-D 选项定义一个宏 NDEBUG。对于上面的例子,我们需要给 MACHINE 定义一个值,可以写成类似这样的命令:gcc -c -DMACHINE=8086 main.c。这种办法需要给每个编译命令都加上适当的选项,和第 2 种方法相比似乎也很麻烦,第 2 种方法在头文件中只写一次宏定义就可以在很多源文件中生效,第 3 种方法能不能做到「只写一次到处生效」呢?等以后学习了 Makefile 就有办法了。

最后通过下面的例子说一下 #if 后面的表达式:

#define VERSION  2
#if defined x || y || VERSION < 3
  1. 首先处理 defined 运算符,defined 运算符一般用作表达式中的一部分,如果单独使用,#if defined x 相当于 #ifdef x,而 #if !defined x 相当于 #ifndef x。在这个例子中,如果 x 这个宏有定义,则把 defined x 替换为 1,否则替换为 0,因此变成 #if 0 || y || VERSION < 3
  2. 然后把有定义的宏展开,变成 #if 0 || y || 2 < 3
  3. 把没有定义的宏替换成 0,变成 #if 0 || 0 || 2 < 3,注意,即使前面定义了一个变量名是 y,在这一步也还是替换成 0,因为 #if 的表达式必须在编译时求值,其中包含的名字只能是宏定义。
  4. 把得到的表达式 0 || 0 || 2 < 3 像 C 表达式一样求值,求值的结果是 #if 1,因此条件成立。

4. 其它预处理特性

#pragma 预处理指示供编译器实现一些非标准的特性,C 标准没有规定 #pragma 后面应该写什么以及起什么作用,由编译器自己规定。有的编译器用 #pragma 定义一些特殊功能寄存器名,有的编译器用 #pragma 定位链接地址,本书不做深入讨论。如果编译器在代码中碰到不认识的 #pragma 指示则忽略它,例如 gcc#pragma 指示都是 #pragma GCC ... 这种形式,用别的编译器编译则忽略这些指示。

C 标准规定了几个特殊的宏,在不同的地方使用可以自动展开成不同的值,常用的有 __FILE____LINE____FILE__ 展开为当前源文件的文件名,是一个字符串,__LINE__ 展开为当前代码行的行号,是一个整数。这两个宏在源代码中不同的位置使用会自动取不同的值,显然不是用 #define 能定义得出来的,它们是编译器内建的特殊的宏。在打印调试信息时打印这两个宏可以给开发者非常有用的提示,例如在第 11 章「排序与查找」第 6 节「折半查找」我们看到 assert 函数打印的错误信息就有 __FILE____LINE__ 的值。现在我们自己实现这个 assert 函数,以理解它的原理。这个实现出自 Standard C Library

例 21.3. assert.h 的一种实现

/* assert.h standard header */
#undef assert	/* remove existing definition */

#ifdef NDEBUG
	#define assert(test)	((void)0)
#else		/* NDEBUG not defined */
	void _Assert(char *);
	/* macros */
	#define _STR(x) _VAL(x)
	#define _VAL(x) #x
	#define assert(test)	((test) ? (void)0 \
		: _Assert(__FILE__ ":" _STR(__LINE__) " " #test))
#endif

通过这个例子可以全面复习本章所讲的知识。C 标准规定 assert 应该实现为宏定义而不是一个真正的函数,并且 assert(test) 这个表达式的值应该是 void 类型的。首先用 #undef assert 确保取消前面对 assert 的定义,然后分两种情况:如果定义了 NDEBUG,那么 assert(test) 直接定义成一个 void ,什么也不做;如果没有定义 NDEBUG,则要判断测试条件 test 是否成立,如果条件成立就什么也不做,如果不成立则调用 _Assert 函数。假设在 main.c 文件的第 33 行调用 assert(is_sorted()),那么 __FILE__ 是字符串 "main.c"__LINE__ 是整数33#test 是字符串 "is_sorted()"。注意 _STR(__LINE__) 的展开过程:首先展开成 _VAL(33),然后进一步展开成字符串 "33"。这样,最后 _Assert 调用的形式是 _Assert("main.c" ":" "33" " " "is_sorted()"),传给 _Assert 函数的字符串是 "main.c:33 is_sorted()"_Assert 函数是我们自己定义的,在另一个源文件中:

/* xassert.c _Assert function */
#include <stdio.h>
#include <stdlib.h>

void _Assert(char *mesg)
{		/* print assertion message and abort */
	fputs(mesg, stderr);
	fputs(" -- assertion failed\n", stderr);
	abort();
}

注意,在头文件 assert.h 中自己定义的内部使用的标识符都以 _ 线开头,例如 _STR_VAL_Assert,因为我们在模拟 C 标准库的实现,在第 2 章「常量、变量和表达式」第 3 节「变量」讲过,以 _ 线开头的标识符通常由编译器和 C 语言库使用,在 /usr/include 下的头文件中你可以看到大量 _ 线开头的标识符。另外一个问题,为什么我们不直接在 assert 的宏定义中调用 fputsabort 呢?因为调用这两个函数需要包含 stdio.hstdlib.h,C 标准库的头文件应该是相互独立的,一个程序只要包含 assert.h 就应该能使用 assert,而不应该再依赖于别的头文件。_Assert 中的 fputs 向标准错误输出打印错误信息,abort 异常终止当前进程,这些函数以后再详细讨论。

现在测试一下我们的 assert 实现,把 assert.hxassert.c 和测试代码 main.c 放在同一个目录下。

/* main.c */
#include "assert.h"

int main(void)
{
	assert(2>3);
	return 0;
}

注意 #include "assert.h" 要用 " 引号而不要用 <> 括号,以保证包含的是我们自己写的 assert.h 而非 C 标准库的头文件。然后编译运行:

$ gcc main.c xassert.c
$ ./a.out
main.c:6 2>3 -- assertion failed
Aborted

在打印调试信息时除了文件名和行号之外还可以打印出当前函数名,C99 引入一个特殊的标识符 __func__ 支持这一功能。这个标识符应该是一个变量名而不是宏定义,不属于预处理的范畴,但它的作用和 __FILE____LINE__ 类似,所以放在一起讲。例如:

例 21.4. 特殊标识符 __func__

#include <stdio.h>

void myfunc(void)
{
	printf("%s\n", __func__);
}

int main(void)
{
	myfunc();
	printf("%s\n", __func__);
	return 0;
}
$ gcc main.c
$ ./a.out 
myfunc
main