[4个规则]
1、用宏定义表达式时,要使用完备的括号。
说明:因为宏只是简单的代码替换,不会像函数一样先将参数计算后,再传递。
示例:如下定义的宏都存在一定的风险
#define RECTANGLE_AREA(a, b) a * b
#define RECTANGLE_AREA(a, b) (a * b)
#define RECTANGLE_AREA(a, b) (a) * (b)
正确的定义应为:
#define RECTANGLE_AREA(a, b) ((a) * (b))
这是因为:
如果定义#define RECTANGLE_AREA(a, b) a * b 或#define RECTANGLE_AREA(a, b) (a * b),则c/RECTANGLE_AREA(a, b) 将扩展成c/a * b , c 与b 本应该是除法运算,结果变成了乘法运算,造成错误。
如果定义#define RECTANGLE_AREA(a, b) (a) * (b),则RECTANGLE_AREA(c + d, e + f)将扩展成: (c + d * e + f), d与e 先运算,造成错误。
2、将宏所定义的多条表达式放在大括号中
说明:更好的方法是多条语句写成do while(0)的方式。
示例:看下面的语句,只有宏的第一条表达式被执行。
#define FOO(x) \
printf("arg is %d\n", x); \
do_something_useful(x);
为了说明问题, 下面for语句的书写稍不符规范
for (blah = 1; blah < 10; blah++)
FOO(blah)
用大括号定义的方式可以解决上面的问题:
#define FOO(x) { \
printf("arg is %s\n", x); \
do_something_useful(x); \
}
但是如果有人这样调用:
if (condition == 1)
FOO(10);
else
FOO(20);
那么这个宏还是不能正常使用,所以必须这样定义才能避免各种问题:
#define FOO(x) do { \
printf("arg is %s\n", x); \
do_something_useful(x); \
} while(0)
用do-while(0)方式定义宏,完全不用担心使用者如何使用宏,也不用给使用者加什么约束。
3、使用宏时,不允许参数发生变化
示例:如下用法可能导致错误。
#define SQUARE(a) ((a) * (a))
int a = 5;
int b;
b = SQUARE(a++); // 结果: a = 7,即执行了两次增。
正确的用法是:
b = SQUARE(a);
a++; // 结果: a = 6,即只执行了一次增。
同时也建议即使函数调用,也不要在参数中做变量变化操作,因为可能引用的接口函数,在某个版本升级后,变成了一个兼容老版本所做的一个宏,结果可能不可预知。
4、不允许直接使用魔鬼数字
说明:使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。
使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点。
解决途径:
对于局部使用的唯一含义的魔鬼数字,可以在代码周围增加说明注释,也可以定义局部const变量,变量命名自注释。
对于广泛使用的数字,必须定义const全局变量/宏;同样变量/宏命名应是自注释的。
0作为一个特殊的数字,作为一般默认值使用没有歧义时,不用特别定义。
[3条建议]
1、除非必要,应尽可能使用函数代替宏
说明:宏对比函数,有一些明显的缺点:
宏缺乏类型检查,不如函数调用检查严格。
宏展开可能会产生意想不到的副作用,如#define SQUARE(a) (a) * (a)这样的定义,如果是SQUARE(i++),就会导致i被加两次;如果是函数调用double square(double a) {return a * a;}则不会有此副作用。
以宏形式写的代码难以调试难以打断点,不利于定位问题。
宏如果调用的很多,会造成代码空间的浪费,不如函数空间效率高。
示例:下面的代码无法得到想要的结果:
#define MAX_MACRO(a, b) ((a) > (b) ? (a) : (b))
int MAX_FUNC(int a, int b) {
return ((a) > (b) ? (a) : (b));
}
int testFunc()
{
unsigned int a = 1;
int b = -1;
printf("MACRO: max of a and b is: %d\n", MAX_MACRO(++a, b));
printf("FUNC : max of a and b is: %d\n", MAX_FUNC(a, b));
return 0;
}
上面宏代码调用中,结果是(a < b),所以a只加了一次,所以最终的输出结果是:
MACRO: max of a and b is: -1
FUNC : max of a and b is: 2
2、常量建议使用const定义代替宏
说明: “尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。看下面的语句:
#define ASPECT_RATIO 1.653
编译器会永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653, 而不是ASPECT_RATIO。如果ASPECT_RATIO不是在你自己写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去。这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不会出现在符号列表中。
解决这个问题的方案很简单:不用预处理宏,定义一个常量:
const double ASPECT_RATIO = 1.653;
这种方法很有效,但有两个特殊情况要注意。首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const外,重要的是指针也经常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const:
const char * const authorName = "Scott Meyers";
延伸阅读材料:关于 const 和 指针 的使用,这里摘录两段ISO/IEC 9899(俗称C99)的描述:
The following pair of declarations demonstrates the difference between a “variable pointer
to a constant value” and a “constant pointer to a variable value”.
const int *ptr_to_constant;
int *const constant_ptr;
The contents of any object pointed to by ptr_to_constant shall not be modified through that
pointer,but ptr_to_constant itself may be changed to point to another object. Similarly, the
contents of the intpointed to by constant_ptrmay be modified, but constant_ptritself shall
always point to the same location.
The declaration of the constant pointer constant_ptr may be clarified by including a
definition for the type “pointer to int”.
typedef int *int_ptr;
const int_ptr constant_ptr;
declares constant_ptras an object that has type “const-qualified pointer to int”.
3、宏定义中尽量不使用return、 goto、 continue、 break等改变程序流程的语句
说明:如果在宏定义中使用这些改变流程的语句,很容易引起资源泄漏问题,使用者很难自己察觉。
示例:在某头文件中定义宏CHECK_AND_RETURN:
#define CHECK_AND_RETURN(cond, ret) {if (cond == NULL_PTR) {return ret;}}
然后在某函数中使用(只说明问题,代码并不完整):
pMem1 = VOS_MemAlloc(...);
CHECK_AND_RETURN(pMem1 , ERR_CODE_XXX)
pMem2 = VOS_MemAlloc(...);
CHECK_AND_RETURN(pMem2 , ERR_CODE_XXX) /*此时如果pMem2==NULL_PTR,则pMem1未释放函数就返
回了,造成内存泄漏。 */
所以说,类似于CHECK_AND_RETURN这些宏,虽然能使代码简洁,但是隐患很大,使用须谨慎。