在严格意义上的编译过程开始之前,C语言预处理器首先对程序代码做了必要的转换处理。预处理器的主要作用是:
- 我们有时需要将某个特定数量在程序中出现的所有实例统统加以修改
- 大多数C语言实现在函数调用时都会带来重大的系统开销
1. 不能忽视宏定义中的空格
#define f (x) ((x)-1)
此时f(x)代表:
(x)((x)-1)
如果希望定义f(x)为((x)-1),必须这样写:
#define f(x) ((x)-1)
空格的规则适用于宏定义,但是不适用于宏调用,所以f(3)和f (3)的结果都等于2
2. 宏并不是函数
宏定义中出现的所有括号,它们的作用时预防引起与优先级有关的问题,如果没有括号求绝对值的定义如下:
#define abs(x) x>0?x:-x
此时abs(a-b)求值的结果将是
a-b>0?a-b:-a-b
-a-b与我们期望的-(a-b)是不一样的。
正确的实现应该是:
#define abs(x) ((x)>=0)?(x):-(x))
即使宏定义中的各个参数与整个结果表达式都被括号括起来,也仍然还可能有其他问题存在,比如说,一个操作数如果在两处被用到,就会被求值两次。比如:
biggest=max(biggest, x[i++]);
在宏展开后将会变成:
biggest=((biggest)>(x[i++])?(biggest):(x[i++]));
i++可能被执行两次,导致错误,解决这个问题的办法:
- 确保max中参数没有像++,--这样的副作用
- 将max使用函数实现
使用宏的另外一个危险是:宏展开可能产生非常庞大的表达式,占用空间远远超过编程者所期望的空间,比如:
3. 宏并不是语句
assert宏的一个错误实现:
#define assert(e) if (!e) assert_error(_FILE_, LINE_)
但这样会导致一些错误:
这个修复方式是把宏体整个括起来,即如下:
#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宏的正确实现如下:
#define assert(e) \
((void)((e)||_assert_error(__FILE__,__LINE__)))
另外一个实现如下:
#define assert(expression) \
((void)((expression) || \
(printf("Assertion failed: %s, file %s, line %d\n", \
#expression, __FILE__, __LINE__), abort(), 0)))
4. 宏并不是类型定义
宏的一个常见用途是使多个不同变量的类型可在一个地方说明,宏定义的这种用法有一个优点——可移植性。但是用宏定义类型会有一些问题,还是推荐使用typedef关键字:
#define T1 struct foo *
typedef struct foo *T2
T1 a,b;
T2 a,b;
其中T1 a,b;将被扩展为:
struct foo * a, b;
这个语句中a被定义为一个指向结构的指针,而b却被定义为一个结构,而不是指针。