[C 陷阱与缺陷] (六) 预处理器

一 预处理器

在严格意义上的编译过程开始之前,C 语言预处理器首先对程序代码作了必要的转换处理。因此,我们运行的程序实际上并不是我们所写的程序。预处理器使得编程者可以简化某些工作,它的重要性可以由两个主要的原因说明(当然还有一些次要原因,此处就不赘述了)。

第一个原因是,我们也许会遇到这样的情况,需要将某个特定数量(例如,某个数据表的大小)在程序中出现的所有实例统统加以修改。我们希望能够通过在程序中,只改动一处数值,然后重新编译就可以实现。预处理器要做到这一点可以说是轻而易举,即使这个数值在程序中的很多地方出现。我们只需要将这个数值定义为一个显式常量(manifest constant), 然后在程序中需要的地方使用这个常量即可。而且,预处理器还能够很容易地把所有常量定义都集中在一起, 这样要找到这些常量也非常容易。

第二个原因是,大多数 C 语言实现在函数调用时都会带来重大的系统开销。因此,我们也许希望有这样一种程序块, 它看上去像一个函数, 但却没有函数调用的开销。举例来说,getchar 和 putchar 经常被实现为宏,以避免在每次执行输入或者输出一个字符这样简单的操作时,都要调用相应的函数而造成系统效率的下降。

虽然宏非常有用,但如果程序员没有认识到宏只是对程序的文本起作用,那么他们很容易对宏的作用感到迷惑。也就是说,宏提供了一种对组成 C 程序的字符进行变换的方式,而并不作用于程序中的对象。因而,宏既可以使一段看上去完全不合语法的代码成为一个有效的 C 程序,也能使一段看 上去无害的代码成为一个可怕的怪物。

1. 不能忽视宏定义中的空格

一个函数如果不带参数,在调用时只需在函数名后加上–对括号即可加以调用了。而一个宏如果不带参数,则只需要使用宏名即可,括号无关紧要。只要宏已经定义过了,就不会带来什么问题:预处理器从宏定义中就可以知道宏调用时是否需要参数。与宏调用相比,宏定义显得有些“暗藏机关”。例如,下面的宏定义中f是否带了一个参数呢?

#define f (x) ((x)-1)

答案只可能有两种:

或者 f(x) 代表 ((x)-1)

或者 f 代表 (x) ((x)-1)

在上述宏定义中,第二个答案是正确的,因为在 f 和后面的 (x) 之间多了一个空格!所以,如果希望定义 f(x) 为 ((x)-1),必须像下面这样写:

#define f(x) ((x)-1)

这一规则不适用于宏调用,而只对宏定义适用。因此,在上面完成宏定义后,f(3) 与 f (3) 求值后 都等于 2。

2. 宏并不是函数

因为宏从表面上看其行为与函数非常相似,程序员有时会禁不住把两者视为完全等同。因此,我们常常可以看到类似下面的写法:

#define abs(x) (((x)>=0)?(x) :-(x))

或者:

#define max(a,b) ((a)>(b)?(a):(b))

请注意宏定义中出现的所有这些括号,它们的作用是预防引起与优先级有关的问题。例如,假设宏 abs 被定义成了这个样子:

#define abs(x) x>0?x:-x

让我们来看 abs(a-b) 求值后会得到怎样的结果。表达式

abs(a-b)

会被展开为

a-b>0?a-b:-a-b

这里的子表达式 -a-b 相当于 (-a)-b,而不是我们期望的 -(a-b),因此上式无疑会得到一个错误的结果。因此,我们最好在宏定义中把每个参数都用括号括起来。同样,整个结果表达式也应该用括号括起来,以防止当宏用于一个更大一些的表
达式中可能出现的问题。如果不这样,

abs(a)+1

展开后的结果为:

a>0?a:-a+1

这个表达式很显然是错误的,我们期望得到的是 -a,而不是 -a+1! abs 的正确定义应该是这样的:

#define abs(x) (((x)>=0)?(x):-(x))

这时,abs (a-b) 才会被正确地展开为: ((a-b)>0? (a-b):-(a-b))
而 abs (a)+1 也会被正确地展开为: ((a)>0?(a):-(a))+1

即使宏定义中的各个参数与整个结果表达式都被括号括起来,也仍然还可能有其他问题存在,比如说,一个操作数如果在两处被用到,就会被求值两次。例如,在表达式 max(a,b )中,如果 a 大于 b,那么 a 将被求值两次:第一次是在 a 与 b 比较期间,第二次是在计算 max 应该得到的结果值时。

这种做法不但效率低下,而且可能是错误的:.

biggest = x[0];
i = 1;
while (i < n)
biggest = max(biggest, x[i++]);

如果max是一个真正的函数,上面的代码可以正常工作;而如果max是一个宏,那么就不能正常工作。要看清楚这一点,我们首先初始化数组x中的一些元素:

x[0] = 2;
x[1] = 3;
x[2] = 1;

然后考察在循环的第一次迭代时会发生什么。上面代码中的赋值语句将被扩展为:

biggest = ( (biggest)>(x[i++])?(biggest):(x[i++]));

首先,变量 biggest 将与 x[i++] 比较。因为 i 此时的值是 1, x[1] 的值是 3,而变量 biggest 此时的值是 x[0] 即 2,所以关系运算的结果为 false (假)。 这里,因为 i++ 的副作用,在比较后 i 递增为2。

因为关系运算的结果为 false (假),所以 x[i++] 的值将被赋给变量 biggest 。然而,经过 i++ 的递增运算后,i 此时的值是 2。所以,实际上赋给变量 biggest 的值是 x[2] ,即 1。这时,又因为 i++ 的副作用,i 的值成为 3 。

解决这类问题的一个办法是,确保宏 max 中的参数没有副作用:

biggest = x[0];
for(i = 1; i < n; i++)
biggest = max(biggest, x[i]);

另一个办法是让 max 作为函数而不是宏,或者直接编写比较两数取较大者的运算的代码:

biggest = x[0];
for(i = 1; i < n; i++)
if(x[i] > biggest)
biggest = x[i];

下面是另外一个例子,其中因为混合了宏和递增运算的副作用,使代码显得岌岌可危。这个例子是宏 putc 的一个典型定义:

#define putc(x,p) \
(--(p)->_cnt>=0?(*(p)->_ ptr++=(x)):_ flsbuf(X,p))

宏 putc 的第一个参数是将要写入文件的字符,第二个参数是一个指针,指向一个用于描述文件的内部数据结构。请注意这里的第一个参数 x,它极有可能是类似于 *z++ 这样的表达式。尽管 x 在宏 putc 的定义中两个不同的地方出现了两次,
但是因为这两次出现的地方是在运算符 : 的两侧,所以 x 只会被求值一次。

第二个参数 p 则恰恰相反,它代表将要写入字符的文件,总是会被求值两次。因为文件参数 p 一般不需要作递增递减之类有副作用的操作,所以这很少引起麻烦。不过, ANSI C 标准中还是提出了警告: putc 的第二个参数可能会被求值两次。

某些 C 语言实现对宏 putc 的定义也许不会像上面的定义那样小心翼翼,putc 的第一个参数很可能被不止一次求值,这样实现是可能的。编程者在给 putc 一个可能有副作用的参数时,应该考虑一下正在使用的 C 语言实现是否足够周密。

再举一个例子,考虑许多 C 库文件中都有的 toupper 函数,该函数的作用是将所有的小写字母转换为相应的大写字母,而其他的字符则保持原状。如果我们假定所有的小写字母和所有的大写字母在机器字符集中都是连续排列的(在大小写字母之间可能有一个固定的间隔),那么我们可以这样实现toupper函数:

toupper (int c)
{
if(c >= 'a' && c <= 'z')
c += 'A' - 'a';

return c;
}

在大多数 C 语言实现中, toupper 的数在调用时造成的系统开销要大大多于函数体内的实际计算操作。因此,实现者很可能禁不住要把 toupper 实现为宏:

#define toupper(c) \ 
((c)>='a' && (c)<='z')? ((c) - 'a' + 'A') : (c))

在许多情况下,这样做确实比把 toupper 实现为函数要快得多。然而,如果编程者试图这样使用

toupper (*p++)

则最后的结果会让所有人都大吃一惊!

使用宏的另一个危险是,宏展开可能产生非常庞大的表达式,占用的空间远远超过了编程者所期望的空间。例如,让我们再看宏max的定义:

#define max(a,b) ((a)>(b)?(a):(b))

假定我们需要使用上面定义的宏 max,来找到 a、b、 c、 d 四个数的最大者,最显而易见的写法是:

max(a, max (b, max(c, d)))

上面的式子展开后就是:

((a)>(((b)>(((c)>(d)?(c):(d)))?(b):(((c)>(d)?(C):()))))?
(a):(((b)>(((c)>(d)?(c): (d))?(b):((c)>(d)?(C):())))))

确实,这个式子太长了!如果我们调整一下,使上式中操作数左右平衡:

max (max(a,b) ,max(c,d) )

现在这个式子展开后还是较长: .

((((a)>(b)?(a):(b)))>(({c)>(d)?(c):(d)))?
(((a)>(b)?(a):(b))):(((c)>(d)?(C):(d))))

其实,写成以下代码似乎更容易一些:

biggest = a;
if (biggest < b) biggest = b;
if (biggest < c) biggest = c;
if (biggest < d) biggest = d;

3. 宏并不是语句

编程者有时会试图定义宏的行为与语句类似,但这样做的实际困难往往令人吃惊! 举例来说,考虑一下assert 宏,它的参数是一个表达式,如果该表达式为 0,就使程序终止执行,并给出一条适当的出错消息。把 assert 作为宏来处理,这
样就使得我们可以在出错信息中包括有文件名和断言失败处的行号。也就是说,

assert (x>y);

在x大于y时什么也不做,其他情况下则会终止程序。

下面是我们定义 assert 宏的第一次尝试:

#define assert(e) if (!e) assert_ error(_ FILE_, _ LINE__ )

因为考虑到宏 assert 的使用者会加上一个分号,所以在宏定义中并没有包括分号。__FILE____LINE__ 是内建于 C语言预处理器中的宏,它们会被扩展为所在文件的文件名和所处代码行的行号。

宏 assert 的这个定义,即使用在一个再明白直接不过的情形中,也会有一些难于察觉的错误:

if(x > 0 && y > 0)
assert(x > y);
else
assert(y > x) ;

上面的写法似乎很合理,但是它展开之后就是这个样子:

if(x>0&&y>0)
if(!(x > y)) assert_error("foo.c", 37);
else
if(!(y > x)) assert_error("foo.c", 39);

把上面的代码作适当的缩排处理,我们就能够看清它实际的流程结构与我们期望的结构有怎样的区别:

if(x>0&&y>0)
if(!(x > y))
assert_error("foo.c", 37);
else
if(!(y > x))
assert_error("foo.c", 39) ;

读者也许会想到,在宏 assert 的定义中用大括号把宏体整个给“括”起来,就能避免这样的问题产生:

#define assert(e)\
{ if (!e) assert_error(__FILE__, __LINE__ ); }

然而,这样做又带来了一个新的问题。我们上面提到的例子展开后就成了:

if(x>0&&y> 0)
{ if(!(x > y)) assert_error("foo.c", 37);};
else
{ if(!(y > x)) assert_error ("foo.c"39);};

在else之前的分号是一个语法错误。要解决这个问题,一个办法是对 assert 的调用后面都不再跟一个分号,但这样的用法显得有些“怪异”:

y = distance(p, q) ;
assert(y > 0)
x = sqrt(y);

宏 assert 的正确定义很不直观,编程者很难想到这个定义不是类似于一个语句,而是类似一个表达式

#define assert(e) \
((void) ((e) || assert_error(__FILE__, __LINE__ ))

这个定义实际上利用了 || 运算符对两侧的操作数依次顺序求值的性质。

如果 e 为真,|| 后半部分语句不会被执行。

4. 宏不是类型定义

宏的类型定义:

#define FOOTYPE struct foo

FOOTYPE a;
FOOTYPE b, c;

宏的这种用法有一个优点——可移植性,得到了所有 C 编译器的支持。

但是我们最好还是使用类型定义:

typedef struct foo FOOTYPE;

这个语句定义了 FOOTYPE 为一个新的类型,与 struct foo 完全等效。

这两种命名类型的方式似乎都差不多,但是使用typedef的方式要更加通用一些。例如,考虑下面的代码:

#define T1 struct foo * 
typedef struct foo *T2;

从上面两个定义来看,T1 和 T2 从概念上完全符同,都是指向结构foo的指针。但是,当我们试图用它们来声明多个变量时,问题就来了:

T1 a, b;
T2 a, b;

第一个声明被扩展为:

struct foo * a,b;

这个语句中 a 被定义为一个指向结构的指针,而 b 却被定义为一个结构(而不是指针)。

第二个声明则不同,它定义了 a 和 b 都是指向结构的指针,因为这里 T2 的行为完全与一个真实的类型相同。

二 练习

1. 练习6-1

请使用宏来实现max的一个版本,其中max的参数都是整数,要求在宏 max 的定义中这些整型参数只被求值一次。

max宏的每个参数的值都有可能使用两次: 一次是在两个参数作比较时;一次是在把它作为结果返回时。因此,我们有必要把每个参数存储在一个临时变量中。

遗憾的是,我们没有直接的办法可以在一个 C表达式的内部声明一个临时变量。因此,如果我们要在一个表达式中使用 max宏,那么我们就必须在其他地方声明这些临时变量,比如说可以在宏定义之后,但不是将这些变量作为宏定义的一部分进行声明。如果 max 宏用于不止一个程序文件,我们应该把这些临时变量声明为 static, 以避免命名冲突。不妨假定,这些定义将出现在某个头文件中:

static int max__ temp1, max_ temp2;
#define max(p,q) (max_temp1=(p), max_temp2=(q),\
max_temp1>max_temp2? max_temp1 : max_temp2)

只要对max宏不是嵌套调用,上面的定义都能正常工作;在 max 宏嵌套调用的情况下,我们不可能做到让它正常工作。

2. 练习6-2

本章第1节中提到的“表达式”

(x) ((x)-1)

能否成为一个合法的C表达式?

一种可能是,如果 x 是类型名,例如 x 被这样定义:

typedef int x;

在这种情况下,

(x) ((x)-1)

等价于

(int) ((int)-1)

这个式子的含义是把常数 -1 转换为 int 类型两次。我们也可以通过预处理指令来定义 x 为一种类型,以达到同样的效果:

#define x int

另一种可能是当 x 为函数指针时。回忆一下,如果某个上下文中本应需要函数而实际上却用了函数指针,那么该指针所指向的函数将会自动地被取得并替换这个函数指针。因此,本题中的表达式可以被解释为调用 x 所指向的函数,这个函数的参数是 (x)-1。为了保证 (x)-1 是一个合法的表达式,x 必须实际地指向一个函数指针数组中的某个元素。

x 的完整类型是什么呢?为了讨论问题方便起见,我们假定 x 的类型是 T, 因此可以如下声明 x:

T x;

显而易见,x 必须是一个指针,所指向的函数的参数类型是 T 。这一点让 T 比较难以定义。下面是最容易想到的办法,但却没有用:

typedef void (*T)(T) ;

因为只有当 T 已经被声明之后,才能这样定义 T! 不过,x 所指向的函数的参数类型并不一定要是 T,而可以是任何 T 可以被转换成的类型。具体来说,void * 类型就完全可以:

typedef void (*T) (void *) ;

这个练习的用意在于说明,对于那些看上去无从着手、形式“怪异”的结构, 我们不应该轻率地一律将其作为错误来处理。

参考资料《C 缺陷与陷阱》