运行的程序并不是我们所写的程序:因为 C 预处理器首先对其进行了转换。出于两个主要原因(和很多次要原因),预处理器为我们提供了一些简化的途径。
#define N (1024)
int array[N];
但是,从另一方面来说。define 存在很多不确定性,导致它在实际应用上出现好多“不靠谱”的情况,而显得“渣”,下面通过一些例子,去看一下这些“不靠谱”的情况。
一个函数如果不带参数,在调用时只需在函数名后加上一对括号即可加以调用了。而一个宏如果不带参数,则只需要使用宏名即可,括号无关紧要。只要宏已经定义过了,就不会带来什么问题:预处理器从宏定义中就可以知道宏调用时是否需要参数。
与宏调用相比,宏定义显得有些”暗藏机关“。
例如,下面的宏定义中 f 是否带了一个参数呢?
#define f (x) ((x)-1) ///< f 与 (x) 之间存在一个空格字符
给两个两个选项:
在上述宏定义中,第二选项才是真正展开的内容,这个是编程者希望的样子吗?显然不是,因为在 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 将被求值两次:第一次是在 a 与 b 比较期间,第二次是在计算 max 应该得到的结果值时。
这种做法不但效率低下,而且可能是错误的:
biggest = x[0];
i = 1;
while (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;
这样,编程者只需在程序中改动一行代码,即可改变 a、b、c 的类型,而与 a、b、c 在程序中的什么地方声明无关。
宏定义的这种用法有一个优点——可移植性,得到了所有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却被定义为一个结构体(不是指针)。第二个声明则不同,它定义了 a 和 b 都是指向结构体的指针,因为这里 T2 的行为完全与一个真实的类型相同。