#define可能是个“渣男”

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

  1. 我们希望可以通过改变一个数字并重新编译程序来改变一个特殊量(如表的大小)。
#define N (1024)
int array[N];
  1. 我们可能希望定义一些东西,它们看起来象函数但没有函数调用所需的运行开销。例如,putchar()getchar() 通常实现为宏以避免对每一个字符的输入输出都要进行函数调用。

但是,从另一方面来说。define 存在很多不确定性,导致它在实际应用上出现好多“不靠谱”的情况,而显得“渣”,下面通过一些例子,去看一下这些“不靠谱”的情况。

不能忽视他的小揪揪(空格字符)

一个函数如果不带参数,在调用时只需在函数名后加上一对括号即可加以调用了。而一个宏如果不带参数,则只需要使用宏名即可,括号无关紧要。只要宏已经定义过了,就不会带来什么问题:预处理器从宏定义中就可以知道宏调用时是否需要参数。
与宏调用相比,宏定义显得有些”暗藏机关“。
例如,下面的宏定义中 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 与 (x) 之间没有空格字符。

对于函数定义来说,这样的空格无关紧要,但对于 define 就不是了,或许,嗯,他对 (x) 的疏远比较小器吧。

他在您背后很不修边幅(表达式展开后的逻辑差异)

由于宏可以象函数那样出现,有些程序员有时就会将它们视为完全等同。因此,我们常常可以看到类似下面的写法:

#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 将被求值两次:第一次是在 ab 比较期间,第二次是在计算 max 应该得到的结果值时。
这种做法不但效率低下,而且可能是错误的:

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

如果 max 是一个真正的函数,上面的代码可以正常工作;而如果 max 是一个宏,那么就不能正常工作。
然后考察在循环的第一次迭代时会发生什么。上面代码中的赋值语句 biggest = max (biggest, x[i++]) 将被扩展为:

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

再举一个例子,它就活生生的在我们身边。
toupper 函数在调用时造成的系统开销要大大多于函数体内的实际计算操作,原系统实现:

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

实现中,使用了大量的条件判断,这样俨然会造成效率的下降,因此,实现者很可能禁不住要把 toupper 实现为宏:

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

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

topper(*p++);

结果估计让你“大吃一惊"。
因此谨记,少使用自变表达式作为参数传参(不管是否在 define 定义的函数,除非你比较清除这函数定义机理),如 i++i– 等。

您不听他说他就翻桌子(悬挂式表达式)

编程者有时会试图定义宏的行为与语句类似,但这样做的实际困难往往令人吃惊!
举例来说,设计一个表达式,如果该表达式为 0,就使程序终止执行,并给出一条适当的出错消息。

assert(x>y);

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

#define assert(e) if(!e) assert_error(_FILE_,_LINE_)

因为考虑到宏 assert 的使用者会加上一个分号,所以在宏定义中并没有包括分号。
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);

解决这种问题,最有效的方法当然是摒弃掉那种 悬挂式条件 的极度不良的书写习惯,其次,可以通过把宏定义 单语句化,如:

#define assert(e) do { if (!e) assert_error(__FILE__, __LINE__); } while (0)

这样的写法还有一个好出,让该函数定义真正的做到”无返回值“,如果调用者误操作希望把他的返回结果赋值给一个变量:

int r = assert(0);

编译器会毫不犹豫的给我们报错。

别指望叫他收衣服的他会收袜子(类型的转义)

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

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

这样,编程者只需在程序中改动一行代码,即可改变 abc 的类型,而与 abc 在程序中的什么地方声明无关。
宏定义的这种用法有一个优点——可移植性,得到了所有C编译器的支持。
但是,我们最好还是使用类型定义:

typedef struct foo FOOTYPE;

这个语句定义了 FOOTYPE 为一个新的类型,struct foo 完全等效。单纯看这个使用似乎与宏定义的使用没有太大区别,但是更多的时候,使用宏定义类型,真的很不靠谱
例如,考虑下面卽代码:

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

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

T1 a, b;
T2 a, b;

这个语句中 a 被定义为一个指向结构的指针,而b却被定义为一个结构体(不是指针)。第二个声明则不同,它定义了 ab 都是指向结构体的指针,因为这里 T2 的行为完全与一个真实的类型相同。

你可能感兴趣的:(嵌入式开发过程中躺过的那些坑,c语言)