C 语言预处理器:问与答

问:我看到在有些程序中 # 单独占一行。这样是合法的吗?

答:是合法的。这就是所谓的空指令,它没有任何作用。一些程序员用空指令作为条件编译模块之间的间隔

1
2
3
4
5
#if INT_MAX < 100000
#
#error int type is too small
#
#endif

当然,空行也可以。不过 # 可以帮助读者看清模块的范围。

问:我不清楚程序中哪些常量需要定义成宏。有没有一些可以参照的规则?

答:一条首要的规则是,除了 0 和 1 以外的每一个数值常量都应该定义成宏。字符常量和字符串常量有一点复杂,因为使用宏来替换字符或字符串常量并不总能够提高程序的可读性。我个人建议在下面的条件下使用宏来替代字符或字面串:(1) 常量被不止一次地使用;(2) 以后可能需要修改常量。根据第二条规则,我不会像这样使用宏:

1
#define NUL '\0'

尽管有些程序员会使用。

问:如果要被“串化”的参数包含 "\ 字符,# 运算符会如何处理?

答:它会将 " 转换为 \"\ 转换为 \\。考虑下面的宏:

1
#define STRINGIZE(x) #x

预处理器会将 STRINGIZE("foo") 替换为 "\"foo\""

问:我无法使下面的宏正常工作:

1
#define CONCAT(x,y) x##y

尽管 CONCAT(a,b) 会如所期望的那样得到 ab,但 CONCAT(a,CONCAT(b,c)) 会给出一个怪异的结果。这是为什么?

答:这是那些连 Kernighan 和 Ritchie 都认为“怪异”的规则引起的。替换列表中依赖 ## 的宏通常不能嵌套调用。这里的问题在于 CONCAT(a,CONCAT(b,c)) 不会按照“正常”的方式扩展——CONCAT(b,c) 首先得出 bc,然后 CONCAT(a,bc) 给出 abc。在替换列表中,位于 ## 运算符之前和之后的宏参数在替换时不被扩展,因此 CONCAT(a,CONCAT(b,c)) 扩展成 aCONCAT(b,c),而不会进一步扩展,这是因为没有名为 aCONCAT 的宏。

有一种办法可以解决这个问题,但不太好看。技巧是再定义一个宏来调用第一个宏:

1
#define CONCAT2(x,y) CONCAT(x,y)

CONCAT2(a,CONCAT2(b,c)) 就会得到我们所期望的结果。在扩展外面的 CONCAT2 调用时,预处理器会同时扩展 CONCAT2(b,c)。这里的区别在于 CONCAT2 的替换列表不包含 ##。如果这个也不行,那也不用担心,这种问题并不是经常会遇到。

顺便提一下,# 运算符也有同样的问题。如果 #x 出现在替换列表中,其中 x 是一个宏参数,其对应的实际参数也不会被扩展。因此,假设 N 是一个代表 10 的宏,且 STR(x) 包含替换列表 #x,那么 STR(N) 扩展的结果为 "N",而不是 "10"。解决的方法与处理 CONCAT 时的类似:再定义一个宏来调用 STR

问:如果预处理器重新扫描时又发现了最初的宏名,会如何处理呢?如下面的例子所示:

1
2
3
4
#define N (2*M)
#define M (N+1)

i = N;  /* infinite loop? */

预处理器会将 N 替换为 (2*M),接着将 M 替换为 (N+1)。预处理器还会再次替换 N,从而导致无限循环吗?

答:一些早期的预处理器确实会进入无限循环,但新的预处理器不会。按照 C 语言标准,如果在扩展宏的过程中原先的宏名重复出现的话,宏名不会再次被替换。下面是对 i 的赋值在预处理之后的形式:

1
i = (2*(N+1));

一些大胆的程序员会通过编写与保留字或标准库中的函数名同名的宏来利用这一行为。以库函数 sqrt 为例。sqrt 函数计算参数的平方根,如果参数为负数则返回一个由实现定义的值。我们可能希望参数为负数时返回 0。由于 sqrt 是标准库函数,我们无法很容易地修改它。但是我们可以定义一个 sqrt 宏,使它在参数为负数时返回 0:

1
2
#undef sqrt
#define sqrt(x) ((x)>=0?sqrt(x):0)

此后预处理器会截获 sqrt 的调用,并将它替换成上面的条件表达式。在扫描宏的过程中条件表达式中的 sqrt 调用不会被替换,因此会被留给编译器处理。(注意在定义 sqrt 宏之前先使用#undef 来删除 sqrt 定义的用法。在定义我们自己的 sqrt 宏之前先删除 sqrt 的定义是一种防御性的措施,以防止库中已经把 sqrt 定义为宏了。)

问:我在使用 __LINE____FILE__ 等预定义宏的时候得到出错消息。我需要包含特定的头吗?

答:不需要。这些宏可以由预处理器自动识别。请确保每个宏名的前后有两个下划线,而不是一个。

问:区分“托管式实现”和“独立式实现”的目的是什么?如果独立式实现连 <stdio.h> 头都不支持,它能有什么用?

答:大多数程序(包括本教程中的程序)都需要托管式实现,这些程序需要底层的操作系统来提供输入/输出和其他基本服务。C 的独立式实现用于不需要操作系统(或只需要很小的操作系统)的程序。例如,编写操作系统内核时需要用到独立式实现(这时不需要传统的输入/输出,因而不需要 <stdio.h>)。独立式实现还可用于为嵌入式系统编写软件。

问:我觉得预处理器就是一个编辑器。它如何计算常量表达式呢?

答:预处理器比你想的要复杂。虽然它不会完全按照编译器的方式去做,但它足够“了解”C 语言,所以能够计算常量表达式。(例如,预处理器认为所有未定义的名字的值为 0。其他的差异太深奥,就不再深入了。)在实际使用中,预处理器常量表达式中的操作数通常为常量、表示常量的宏或 defined 运算符的应用。

问:既然我们可以使用 #if 指令和 defined 运算符达到同样的效果,为什么 C 语言还提供 #ifdef 指令和 #ifndef 指令?

答:#ifdef 指令和 #ifndef 指令从 20 世纪 70 年代就存在于 C 语言中了,而 defined 运算符则是在 20 世纪 80 年代的标准化过程中加到 C 语言中的。因此,实际的问题是,为什么将 defined 运算符加到 C 语言中?答案就是 defined 增加了灵活性。我们现在可以使用 #ifdefined 运算符来测试任意数量的宏,而不再是只能使用 #ifdef#ifndef 对一个宏进行测试。例如,下面的指令检查是否 FOOBAR 被定义了而 BAZ 没有被定义:

1
#if defined(FOO) && defined(BAR) && !defined(BAZ)

问:我想编译一个还没有写完的程序,因此我“条件屏蔽”未完成的部分:

1
2
3
#if 0
...
#endif

编译的时候,我得到了一条指向 #if#endif 之间某一行的出错消息。预处理器不是简单地忽略 #if 指令和 #endif 指令之间的所有行吗?

答:不是的,这些代码行不会被完全忽略。在执行预处理指令前,先处理注释,并把源代码分为多个预处理记号。因此,#if#endif 之间未终止的注释会引起出错消息。此外,不成对的单引号或双引号字符也可能导致未定义的行为。

请参阅

(完)

comments powered by Disqus