C语言宏定义的一些总结


较大的C语言项目都会用大量的宏定义来组织代码,比较经典的代码就是Linux Kernel的头文件中用的宏定义。看起来宏展开就是做个替换而已,其实里面有比较复杂的规则,有关宏展开的语法规则此小结力图整理的比较全面。

Object-like Macro

#define N 20
#define STR "hello, world"

上面源码所示的被称为Object-like Macro.

Function Macro

#define MAX(a, b) ((a)>(b)?(a):(b))
k = MAX(i&0x0f, j&0x0f)

上面源码所示的被称为Function-like Macro,注意这种函数式宏定义和真正的函数调用有什么不同:

  1. 函数式宏定义的参数没有类型,预处理器只负责做形式上的替换,而不做参数类型检查,所以传参时要格外小心。
  2. 调用真正函数的代码和调用函数式宏定义的代码编译生成的指令不同。如果MAX是个真正的函数,那么它的函数体return a > b ? a : b;要编译生成指令,代码中出现的每次调用也要编译生成传参指令和call指令。而如果MAX是个函数式宏定义,这个宏定义本身倒不必编译生成指令,但是代码中出现的每次调用编译生成的指令都相当于一个函数体,而不是简单的几条传参指令和call指令。所以,使用函数式宏定义编译生成的目标文件会比较大。
  3. 定义这种宏要格外小心,如果上面的定义写成#define MAX(a, b) (a>b?a:b),省去内层括号,则宏展开就成了k = (i&0x0f>j&0x0f?i&0x0f:j&0x0f),运算的优先级就错了。同样道理,这个宏定义的外层括号也是不能省的,想一想为什么。
  4. 调用函数时先求实参表达式的值再传给形参,如果实参表达式有Side Effect,那么这些Side Effect只发生一次。例如MAX(++a, ++b),如果MAX是个真正的函数,a和b只增加一次。但如果MAX是上面那样的宏定义,则要展开成k = ((++a)>(++b)?(++a):(++b)),a和b就不一定是增加一次还是两次了。
  5. 即使实参没有Side Effect,使用函数式宏定义也往往会导致较低的代码执行效率。

尽管函数式宏定义和真正的函数相比有很多缺点,但只要小心使用还是会显著提高代码的执行效率,毕竟省去了分配和释放栈帧、传参、传返回值等一系列工作,因此那些简短并且被频繁调用的函数经常用函数式宏定义来代替实现。例如C标准库的很多函数都提供两种实现,一种是真正的函数实现,一种是宏定义实现,这一点以后还要详细解释。函数式宏定义经常写成这样的形式(取自内核代码include/linux/pm.h):

#define device_init_wakeup(dev,val) \
        do { \
                device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val); \
        } while(0)

当我第一次看到这个源码的时候,很奇怪为什么都用do{...}while(0)将函数式宏定义括起来。下面我们来分析分析如果不用while括起来会发生什么事情,如下源码所示:

/* main.c */
#define device_init_wakeup(dev,val) \
                { device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val); }

int main(int argc, char *argv[]) {
    if (n > 0)
	device_init_wakeup(d, v);
    else
	continue;
    return 0;
}

接下来可以用gcc -E main.c 来查看预处理之后的源码,

int main(int argc, char *argv[]) {
    if (n > 0)
	/*device_init_wakeup(d, v);*/
        {device_can_wakeup(d) = !!(v);
         //; leads to get the end of the if condition and get error
         device_set_wakeup_enable(d, v);};
    else
	continue;
    return 0;
}

宏定义替换之后可以看出分号导致if条件判断提前结束,else分支找不到对应的if分支,这样就会导致无法正确编译。
另外,需要注意的是如果在一个程序文件中重复定义一个宏,C语言规定这些重复的宏定义必须一模一样。例如这样的重复定义是允许的:

#define OBJ_LIKE (1 - 1)
#define OBJ_LIKE /* comment */ (1/* comment */-/* comment */  1)/* comment */
#define OBJ_LIKE (1-1)
/* 用#undef取消原来的定义,再重新定义 */
#define X 3
... /* X is 3 */
#undef X
... /* X has no definition */
#define X 2
... /* X is 2 */

Operator # and ##

  • 在函数式宏定义中,#运算符用于创建字符串,#运算符后面应该跟一个形参(中间可以有空格或Tab),例如:
#define STR(s) # s
STR(hello 	world)

用cpp命令预处理之后是”hello␣world”,自动用”号把实参括起来成为一个字符串,并且实参中的连续多个空白字符被替换成一个空格。

  • 在宏定义中可以用##运算符把前后两个预处理Token连接成一个预处理Token,和#运算符不同,##运算符不仅限于函数式宏定义,变量式宏定义也可以用。例如:
#define CONCAT(a, b) a##b
CONCAT(con, cat)

预处理之后是concat。我们知道printf函数带有可变参数,函数式宏定义也可以带可变参数,同样是在参数列表中用…表示可变参数。例如:

#define showlist(...) printf(#__VA_ARGS__)
#define report(test, ...) ((test)?printf(#test):\
	printf(__VA_ARGS__))
showlist(The first, second, and third items.);
report(x>y, "x is %d but y is %d", x, y);

/* 宏定义*/
printf("The first, second, and third items.");
((x>y)?printf("x>y"): printf("x is %d but y is %d", x, y));
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s