C语⾔宏的特殊⽤法和⼏个坑(转)
总结⼀下C语⾔中宏的⼀些特殊⽤法和⼏个容易踩的坑。由于本⽂主要参考GCC⽂档,某些细节(如宏参数中的空格是否处理之类)在别的编译器可能有细微差别,请参考相应⽂档。
宏基础
宏仅仅是在C预处理阶段的⼀种⽂本替换⼯具,编译完之后对⼆进制代码不可见。基本⽤法如下:
1. 标⽰符别名
#define BUFFER_SIZE 1024
预处理阶段,foo = (char *) malloc (BUFFER_SIZE);会被替换成foo = (char *) malloc (1024);
宏体换⾏需要在⾏末加反斜杠\
#define NUMBERS 1, \
2, \
3
预处理阶段int x[] = { NUMBERS };会被扩展成int x[] = { 1, 2, 3 };
2. 宏函数
宏名之后带括号的宏被认为是宏函数。⽤法和普通函数⼀样,只不过在预处理阶段,宏函数会被展开。优点是没有普通函数保存寄存器和参数传递的开销,展开后的代码有利于CPU cache的利⽤和指令预测,速度快。缺点是可执⾏代码体积⼤。
#define min(X, Y)  ((X) < (Y) ? (X) : (Y))
y = min(1, 2);会被扩展成y = ((1) < (2) ? (1) : (2));
宏特殊⽤法
1. 字符串化(Stringification)
在宏体中,如果宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如:
#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中,如果断⾔失败,可以将失败的语句输出到反馈信息中
2. 连接(Concatenation)
在宏体中,如果宏体所在标⽰符中有##,那么在宏体扩展的时候,宏参数会被直接替换到标⽰符中。如:
#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 },
...
};
这样就节省了⼤量时间,提⾼效率。
⼏个坑
1. 语法问题
由于是纯⽂本替换,C预处理器不对宏体做任何语法检查,像缺个括号、少个分号神马的预处理器是不管的。这⾥要格外⼩⼼,由此可能引出各种奇葩的问题,⼀下还很难到根源。
2. 算符优先级问题
不仅宏体是纯⽂本替换,宏参数也是纯⽂本替换。有以下⼀段简单的宏,实现乘法:
#define MULTIPLY(x, y) x * y
define的基本用法
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),优先级正常了。
其实这个问题和下⾯要说到的某些问题都属于由于纯⽂本替换⽽导致的语义破坏问题,要格外⼩⼼。
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)
4. 宏参数重复调⽤
有如下宏定义:
#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内修改了全局或静态变量),程序会产⽣逻辑错误。
所以,尽量不要在宏参数中传⼊函数调⽤。
5. 对⾃⾝的递归引⽤
有如下宏定义:
#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))。
注意,这是极不推荐的写法,程序可读性极差。
6. 宏参数预处理
宏参数中若包含另外的宏,那么宏参数在被代⼊到宏体之前会做⼀次完全的展开,除⾮宏体中含有#或##。
有如下宏定义:
#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。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。