C语言的宏,因为缺乏完备的类型检查。常常为很多程序员所诟病。但任何事物都有其利与弊的两面,同样宏也不例外。宏的强大作用在于在编译期自动地为我们产生代码。如果说模板可以通过类型替换来为我们产生类型层面上的多态,那么宏就可以通过符号替换在符号层面上产生的多态。正确合理地使用宏,可以有效地提高代码的可读性,减少代码的维护成本。
不过,宏的使用确实存在着诸多的陷阱,如果不注意,宏就有可能真的变成C++代码的“万恶之首”。本文将为你详细阐述#define使用的陷阱。
1. 由操作符优先级引起的问题
由于宏只是简单的替换,宏的参数如果是复合结构,那么通过替换之后可能由于各个参数之间的操作符优先级高于单个参数内部各部分之间相互作用的操作符优先级,如果不用括号保护各个宏参数,可能会产生预想不到的情形。例如:
#define ceil_div(x, y) (x + y - 1) / y
那么
a = ceil_div( b & c, sizeof(int) );
将被转化为:
a = ( b & c + sizeof(int) - 1) / sizeof(int);
// 由于+/-的优先级高于&的优先级,那么上面式子等同于:
a = ( b & (c + sizeof(int) - 1)) / sizeof(int);
这显然不是我们的初衷。为了避免这种情况发生,应当多写几个括号:
#define ceil_div(x, y) (((x) + (y) - 1) / (y))
2. 使用宏定义,不允许参数发生变化
这也是带参数的宏定义和函数的区别,有人认为带参数的宏和函数具有同样的功能,只是实现方式的不同,实际是是这样的吗?下面的代码示例,将向你展示宏和函数的差异。
#include <stdio.h>
#define sqrt(a) ((a)*(a))
// 计算平方值
int fsqrt(int a)
{
return a*a;
}
int main()
{
int a = 10, b = 10;
int r1, r2;
r1 = sqrt(a++);
r2 = fsqrt(b++);
printf("a = %d, b = %d, r1 = %d, r2 = %d\n", a, b, r1, r2);
return 0;
}
在VC6.0下,这段程序最终结果是a = 12;b = 11;r1 = 100;r2 = 100;之所以a变成12,是因为在替换的时候,a++被执行了两次。要避免这种行为,就要使宏参数不发生变化。如:a++;r1 = sqrt(a),一切就ok了!
3. 使用do{}while(false)将宏定义包含的多条表达式放到大括号里
使用宏时,如果宏包含多个语句,一定要用大括号把宏括起来。以防在某些情况下宏定义的多条语句只有一条语句被执行。下面这段代码就为你展示了这个问题:
#include <stdio.h>
// 变量初始化宏
#define INITIAL(a, b)\
a = 0;\
b = 0;
int main()
{
int a[5], b[5] ;
int i = 0;
for(i=0; i<5; i++)
{
INITIAL(a[i], b[i]);
}
printf("a = %d, b = %d\n", a[0], b[0]);
return 0;
}
结果打印a是正常的,但打印b却是未初始化的结果。因为简单的文本替换,不能保证多条表达式都放到for循环体内。上述的宏定义应改为:
// 变量初始化宏
#define INITIAL(a, b)\
{\
a = 0;\
b = 0;\
}
注意这个“\”号哦,表示下面的行和当前行在预编译时被认为在同一行。将宏实现改成上述实现,此代码确实可正常运行了。但这并不是最好的方案,其依然存在风险(请参考实用经验44 do{}while说明)。建议修改为:
// 变量初始化宏
#define INITIAL(a, b)\
do{\
a = 0;\
b = 0;\
}while(false)
4. 关于…的使用
…在C宏中称为Variadic Macro,也就是变参宏。例如:
#define myprintf(templt, ...) fprintf(stderr,templt,__VA_ARGS__)
#define myprintf(templt, args...) fprintf(stderr,templt,args)
第一个宏中由于没有对变参起名,我们用默认的宏__VA_ARGS__来替代它。第二个宏中,我们显式地命名变参为args,那么我们在宏定义中就可以用args来代指变参了。同C语言的stdcall一样,变参必须作为参数表的最有一项出现。当上面的宏中我们只能提供第一个参数templt时,C标准要求我们必须写成: myprintf(templt,…);的形式。这时的替换过程为: myprintf(“Error!”,);替换为fprintf(stderr,“Error!”,);
这是一个语法错误,不能正常编译。这个问题一般有两个解决方法。GNU G++提供的解决方法允许上面的宏调用写成:
myprintf(templt);
而它将会被通过替换变成:
fprintf(stderr,"Error!",);
很明显,这里仍然会产生编译错误(非本例的某些情况下不会产生编译错误)。除了这种方式外,c99和GNU g++ 都支持下面的宏定义方式:
#define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)
这时,##这个连接符号充当的作用就是当__VAR_ARGS__为空的时候,消除前面的那个逗号。那么此时的翻译过程如下: myprintf(templt);被转化为:fprintf(stderr,templt);这样如果templt合法,将不会产生编译错误。 错误的嵌套宏定义会导致不完整的、配对的括号,但是为了避免出错并且提高可读性,最好避免这样使用。
5. 消除多余的分号
通常情况下,为了使函数模样的宏在表面上看起来像一个通常的C语言调用一样,通常情况下我们在宏的后面加上一个分号,例如下面的带参宏:
MY_MACRO(x);
但是,如果是下面的这种情况:
#define MY_MACRO(x)
{
/* line 1 */
/* line 2 */
/* line 3 */
}
//...
if (condition())
MY_MACRO(a);
else
{
...
}
这样会由于多出的那个分号产生编译错误。为了避免这种情况出现同时保持MY_MACRO(x);的这种写法,我们需要把宏定义为这种形式:
#define MY_MACRO(x) do
{
/* line 1 */
/* line 2 */
/* line 3 */
} while(0)
这样只要保证总是使用分号,就不会有任何问题。
请谨记
- #define定义的带参数的宏,并不是函数。虽然他长得像函数。其实他更像inline函数。
- 定义宏时,请尽量的考虑所有的可能使用环境。以防产生宏副作用。