C语言:陷阱和缺陷_6

发表于:2007-05-25来源:作者:点击数: 标签:缺陷程序行的处理器陷阱
6 预处理器 运行的程序并不是我们所写的程序:因为C预处理器首先对其进行了转换。出于两个主要原因(和很多次要原因),预处理器为我们提供了一些简化的途径。 首先,我们希望可以通过改变一个数字并重新编译程序来改变一个特殊量(如表的大小)的所有实例
6 预处理器

    运行的程序并不是我们所写的程序:因为C预处理器首先对其进行了转换。出于两个主要原因(和很多次要原因),预处理器为我们提供了一些简化的途径。

    首先,我们希望可以通过改变一个数字并重新编译程序来改变一个特殊量(如表的大小)的所有实例脚注[9]

    其次,我们可能希望定义一些东西,它们看起来象函数但没有函数调用所需的运行开销。例如,putchar()和getchar()通常实现为宏以避免对每一个字符的输入输出都要进行函数调用。



6.1 宏不是函数

    由于宏可以象函数那样出现,有些程序员有时就会将它们视为等价的。因此,看下面的定义:

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

注意宏体中所有的括号。它们是为了防止出现a和b是带有比>优先级低的表达式的情况。

    一个重要的问题是,像max()这样定义的宏每个操作数都会出现两次并且会被求值两次。因此,在这个例子中,如果a比b大,则a就会被求值两次:一次是在比较的时候,而另一次是在计算max()值的时候。

    这不仅是低效的,还会发生错误:

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

当max()是一个真正的函数时,这会正常地工作,但当max()是一个宏的时候会失败。譬如,假设x[0]是2、x[1]是3、x[2]是1。我们来看看在第一次循环时会发生什么。赋值语句会被扩展为:

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

首先,biggest与x[i++]进行比较。由于i是1而x[1]是3,这个关系是“假”。其副作用是,i增长到2。

    由于关系是“假”,x[i++]的值要赋给biggest。然而,这时的i变成2了,因此赋给biggest的值是x[2]的值,即1。

    避免这些问题的方法是保证max()宏的参数没有副作用:

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

    还有一个危险的例子是混合宏及其副作用。这是来自UNIX第八版的<stdio.h>中putc()宏的定义:

#define putc(x, p) (--(p)->_cnt >= 0 ? (*(p)->_ptr++ = (x)) : _flsbuf(x, p))

putc()的第一个参数是一个要写入到文件中的字符,第二个参数是一个指向一个表示文件的内部数据结构的指针。注意第一个参数完全可以使用如*z++之类的东西,尽管它在宏中两次出现,但只会被求值一次。而第二个参数会被求值两次(在宏体中,x出现了两次,但由于 它的两次出现分别在一个:的两边,因此在putc()的一个实例中它们之中有且仅有一个被求值)。由于putc()中的文件参数可能带有副作用,这偶尔会出现问题。不过,用户手册文档中提到:“由于putc()被实现为宏,其对待stream可能会具有副作用。特别是putc(c, *f++)不能正确地工作。”但是putc(*c++, f)在这个实现中是可以工作的。

    有些C实现很不小心。例如,没有人能正确处理putc(*c++, f)。另一个例子,考虑很多C库中出现的toupper()函数。它将一个小写字母转换为相应的大写字母,而其它字符不变。如果我们假设所有的小写字母和所有的大写字母都是相邻的(大小写之间可能有所差距),我们可以得到这样的函数:

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

在很多C实现中,为了减少比实际计算还要多的调用开销,通常将其实现为宏:

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

很多时候这确实比函数要快。然而,当你试着写toupper(*p++)时,会出现奇怪的结果。

    另一个需要注意的地方是使用宏可能会产生巨大的表达式。例如,继续考虑max()的定义:

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

假设我们这个定义来查找a、b、c和d中的最大值。如果我们直接写:

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

它将被扩展为:

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

这出奇的庞大。我们可以通过平衡操作数来使它短一些:

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;

比较好一些。

6.2 宏不是类型定义

    宏的一个通常的用途是保证不同地方的多个事物具有相同的类型:

#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b, c;

这允许程序员可以通过只改变程序中的一行就能改变a、b和c的类型,尽管a、b和c可能声明在很远的不同地方。

    使用这样的宏定义还有着可移植性的优势——所有的C编译器都支持它。很多C编译器并不支持另一种方法:

typedef struct foo FOOTYPE;

这将FOOTYPE定义为一个与struct foo等价的新类型。

    这两种为类型命名的方法可以是等价的,但typedef更灵活一些。例如,考虑下面的例子:

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

这两个定义使得T1和T2都等价于一个struct foo的指针。但看看当我们试图在一行中声明多于一个变量的时候会发生什么:

T1 a, b;
T2 c, d;

第一个声明被扩展为:

struct foo * a, b;

这里a被定义为一个结构指针,但b被定义为一个结构(而不是指针)。相反,第二个声明中c和d都被定义为指向结构的指针,因为T2的行为好像真正的类型一样。


脚注
    1. 本文是基于图书《C Traps and Pitfalls》(Addison-Wesley, 1989, ISBN 0-201-17928-8)的一个扩充,有兴趣的读者可以读一读它。
    2. 因为!=的结果不是1就是0。
    3. 感谢Guy Harris为我指出这个问题。
    4. Dennis Ritchie和Steve Johnson同时向我指出了这个问题。
    5. 感谢一位不知名的志愿者提出这个问题。
    6. 感谢Richard Stevens指出了这个问题。
    7. 一些C编译器要求每个外部对象仅有一个定义,但可以有多个声明。使用这样的编译器时,我们何以很容易地将一个声明放到一个包含文件中,并将其定义放到其它地方。这意味着每个外部对象的类型将出现两次,但这比出现多于两次要好。
    8. 分离函数参数用的逗号不是逗号运算符。例如在f(x, y)中,x和y的获取顺序是未定义的,但在g((x, y))中不是这样的。其中g只有一个参数。它的值是通过对x进行求值、抛弃这个值、再对y进行求值来确定的。
    9. 预处理器还可以很容易地组织这样的显式常量以能够方便地找到它们。
    10. PDP-11和VAX-11是数组设备集团(DEC)的商标。



原文转自:http://www.ltesting.net

评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)