[C陷阱和缺陷] 第6章 预处理器

  在严格意义上的编译过程开始之前,C语言预处理器首先对程序代码作了必要的转换处理。因此,我们运行的程序实际上并不是我们所写的程序。预处理器使得编程者可以简化某些工作,它的重要性可以由两个主要的原因说明:
  第一个原因是,我们也许会遇到这样的情况,需要将多个在程序中出现的所有实例统统加以修改。我们希望在程序中只改动一处数值,然后重新编译就可以实现。预处理器要做到这一点可以说是轻而易举(使用宏定义即可: #define N 1024)。而且,预处理器还能够很容易地把所有常量定义都集中在一起,这样要找到这些常量也非常容易。
  第二个原因是,大多数C语言实现在函数调用时都会带来重大的系统开销。因此,我们也许希望有这样一种程序块,它看上去像一个函数,但却没有函数调用的开销。举例来说, getchar 和 putchar 经常被实现为宏,以避免每次执行输入或输出一个字符这样简单的操作时,都要调用相应的函数而造成系统效率的下降。(注:C++的内联函数也有这种异曲同工之妙)
  虽然宏非常有用,但如果程序员没有认识到宏只是对程序文本的替换,那么他们很容易对宏的作用感到迷惑。因而,宏既可以使一段看上去不合语法的代码成为一个有效的C程序,也能使一段看上去无害的代码成为一个可怕的怪物。

6.1 不能忽视宏定义中的空格

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

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

  答案只可能有两种: 调用 f(x) 或者代表

    ( (x)-1 )

  或者代表

    (x)( (x)-1 )

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

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

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

6.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 替换x )

    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++]) );

  会发现由于 i++ 的存在,经过一次循环后,赋给 biggest 的值是 x[2] ,即1。这时,又因为 i++ 的副作用,i 的值成为3。
  解决这类问题的一个办法是,确保宏 max 中的参数没有副作用:

    biggest = x[0];
    i = 1;

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

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

    biggest = x[0];
    i = 1;

    while (i < n)
    {
        if( x[i] > biggest )
            biggest = x[i];
    }

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

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

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

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

  上面展开就是个非常庞大的表达式了,还不如写成函数:

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


6.3 宏并不是语句

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

    assert(x > y);

在 x 大于 y 时什么也不做,其他情况下则会终止程序。
 
  下面是我们定义 assert 宏的第一次尝试:

    #defien  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);

  读者也许会想到,在宏 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 的调用后面都不再跟一个问号,但这样的用法显得有些“怪异”:

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

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

    #define  assert(e) \
                    ( (void) ( (e) || _assert_error(__FILE, __LINE__) ) )                    

  这个定义实际上利用了 || 运算符对两侧的操作符依次顺序求值的性质。如果 e 为 true,就不会执行_assert_error(__FILE, LINE) 了,否则 e 为 false,则会执行 _assert_error(__FILE, LINE) ,并打印出
一条恰当的“断言失败”的出错消息。

6.4 宏并不是类型定义

  宏的一个常见用途是,使多个不同变量的类型可在一个地方说明:

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

  这样,编程者只需要在程序中改动一行代码,即可改变 a、b、c、的类型,而与a、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 的行为完全与一个真实的类型相同。
 

练习

  练习6-1 请使用宏来实现 max 的一个版本,其中 max 的参数都是整数,要求在宏 max 的定义中这些整型参数都只被求值一次。
 
  max 宏的每个参数的值都有可能使用两次:一次是在两个参数作比较时;一次是在把它作为结果返回时。因此,我们有必要把每个参数存储在一个临时变量中,这样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 宏嵌套调用的情况下,我们不可能做到让他正常工作。
 
  练习6-2 本章第1节中提到的表达式 (x) ( (x) -1 ) 能否称为一个合法的C表达式?
 
  一种可能是,如果 x 是类型名,例如 x 被这样定义:

    typedef    int    x;

在这种情况下, (x) ( (x) -1 ) 等价于:

    (int)   ( (int) -1 )

这个式子的含义是把常数 -1 转换为 int 类型两次。
  另一种可能是当 x 为函数指针:

    typedef    void    (*T) (void  *);

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

转载于:https://www.cnblogs.com/linuxAndMcu/p/10079481.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值