总结一下C语言中宏的一些特殊用法和几个容易踩的坑。因为本文主要参考GCC文档,某些细节(如宏参数中的空格是否处理之类)在别的编译器可能有细微差异,请参考相应文档。函数
宏仅仅是在C预处理阶段的一种文本替换工具,编译完以后对二进制代码不可见。基本用法以下:工具
#define BUFFER_SIZE 1024
预处理阶段,foo = (char *) malloc (BUFFER_SIZE);
会被替换成foo = (char *) malloc (1024);
ui
宏体换行须要在行末加反斜杠\spa
#define NUMBERS 1, \
2, \ 3
预处理阶段int x[] = { NUMBERS };
会被扩展成int x[] = { 1, 2, 3 };
code
宏名以后带括号的宏被认为是宏函数。用法和普通函数同样,只不过在预处理阶段,宏函数会被展开。优势是没有普通函数保存寄存器和参数传递的开销,展开后的代码有利于CPU cache的利用和指令预测,速度快。缺点是可执行代码体积大。递归
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
y = min(1, 2);
会被扩展成y = ((1) < (2) ? (1) : (2));
ip
在宏体中,若是宏参数前加个#
,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如:内存
#define WARN_IF(EXP) \
do { if (EXP) \ fprintf (stderr, "Warning: " #EXP "\n"); } \ while (0)
WARN_IF (x == 0);
会被扩展成:文档
do { if (x == 0) fprintf (stderr, "Warning: " "x == 0" "\n"); } while (0);
这种用法能够用在assert中,若是断言失败,能够将失败的语句输出到反馈信息中字符串
在宏体中,若是宏体所在标示符中有##
,那么在宏体扩展的时候,宏参数会被直接替换到标示符中。如:
#define COMMAND(NAME) { #NAME, NAME ## _command }
struct command { char *name; void (*function) (void); };
在宏扩展的时候
struct command commands[] = { COMMAND (quit), COMMAND (help), ... };
会被扩展成:
struct command commands[] = { { "quit", quit_command }, { "help", help_command }, ... };
这样就节省了大量时间,提升效率。
因为是纯文本替换,C预处理器不对宏体作任何语法检查,像缺个括号、少个分号神马的预处理器是无论的。这里要格外当心,由此可能引出各类奇葩的问题,一下还很难找到根源。
不只宏体是纯文本替换,宏参数也是纯文本替换。有如下一段简单的宏,实现乘法:
#define MULTIPLY(x, y) x * y
MULTIPLY(1, 2)
没问题,会正常展开成1 * 2
。有问题的是这种表达式MULTIPLY(1+2, 3)
,展开后成了1+2 * 3
,显然优先级错了。
在宏体中,给引用的参数加个括号就能避免这问题。
#define MULTIPLY(x, y) (x) * (y)
MULTIPLY(1+2, 3)
就会被展开成(1+2) * (3)
,优先级正常了。
其实这个问题和下面要说到的某些问题都属于因为纯文本替换而致使的语义破坏问题,要格外当心。
有以下宏定义:
#define SKIP_SPACES(p, limit) \
{ char *lim = (limit); \ while (p < lim) { \ if (*p++ != ' ') { \ p--; break; }}}
假设有以下一段代码:
if (*p != 0) SKIP_SPACES (p, lim); else ...
一编译,GCC报error: ‘else’ without a previous ‘if’
。原来这个看似是一个函数的宏被展开后是一段大括号括起来的代码块,加上分号以后这个if逻辑块就结束了,因此编译器发现这个else没有对应的if。
这个问题通常用do ... while(0)
的形式来解决:
#define SKIP_SPACES(p, limit) \
do { char *lim = (limit); \ while (p < lim) { \ if (*p++ != ' ') { \ p--; break; }}} \ while (0)
展开后就成了
if (*p != 0) do ... while(0); else ...
这样就消除了分号吞噬问题。
这个技巧在Linux内核源码里很常见,好比这个置位宏#define SET_REG_BIT(reg, bit) do { (reg |= (1 << (bit))); } while (0)
(位于arch/mips/include/asm/mach-pnx833x/gpio.h)
有以下宏定义:
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
当有以下调用时next = min (x + y, foo (z));
,宏体被展开成next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));
,能够看到,foo(z)
被重复调用了两次,作了重复计算。更严重的是,若是foo是不可重入的(foo内修改了全局或静态变量),程序会产生逻辑错误。
因此,尽可能不要在宏参数中传入函数调用。
有以下宏定义:
#define foo (4 + foo)
按前面的理解,(4 + foo)
会展开成(4 + (4 + foo)
,而后一直展开下去,直至内存耗尽。可是,预处理器采起的策略是只展开一次。也就是说,foo
只会展开成(4 + foo)
,而展开以后foo
的含义就要根据上下文来肯定了。
对于如下的交叉引用,宏体也只会展开一次。
#define x (4 + y)
#define y (2 * x)
x
展开成(4 + y) -> (4 + (2 * x))
,y
展开成(2 * x) -> (2 * (4 + y))
。
注意,这是极不推荐的写法,程序可读性极差。
宏参数中若包含另外的宏,那么宏参数在被代入到宏体以前会作一次彻底的展开,除非宏体中含有#
或##
。
有以下宏定义:
#define AFTERX(x) X_ ## x
#define XAFTERX(x) AFTERX(x) #define TABLESIZE 1024 #define BUFSIZE TABLESIZE
AFTERX(BUFSIZE)
会被展开成X_BUFSIZE
。由于宏体中含有##
,宏参数直接代入宏体。XAFTERX(BUFSIZE)
会被展开成X_1024
。由于XAFTERX(x)
的宏体是AFTERX(x)
,并无#
或##
,因此BUFSIZE
在代入前会被彻底展开成1024
,而后才代入宏体,变成X_1024
。