宏、常量

规则 5.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))

规则 5.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)方式定义宏,完全不用担心使用者如何使用宏,也不用给使用者加什么约束。

规则 5.3 使用宏时,不允许参数发生变化。

示例:如下用法可能导致错误。

#define SQUARE(a) ((a) \* (a))
int a = 5;
int b;
b = SQUARE(a++); // 结果:a = 7,即执行了两次增。
正确的用法是:
b = SQUARE(a);
a++; // 结果:a = 6,即只执行了一次增。

同时也建议即使函数调用,也不要在参数中做变量变化操作,因为可能引用的接口函数,在某个版本升级后,变成了一个兼容老版本所做的一个宏,结果可能不可预知。

规则 5.4 不允许直接使用魔鬼数字。

说明:使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。

使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点。

解决途径: 对于局部使用的唯一含义的魔鬼数字,可以在代码周围增加说明注释,也可以定义局部 const 变量,变量命名自注释。

对于广泛使用的数字,必须定义 const 全局变量/宏;同样变量/宏命名应是自注释的。

0 作为一个特殊的数字,作为一般默认值使用没有歧义时,不用特别定义。

建议 5.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

建议 5.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";

建议 5.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 这些宏,虽然能使代码简洁,但是隐患很大,使用须谨慎。