C语言宏的特殊用法和几个坑

原文请见 C语言宏的特殊用法和几个坑

  • 一宏基础
    • 标示符别名
    • 宏函数
  • 二宏特殊用法
    • 字符串化Stringification
    • 连接Concatenation
  • 二几个坑
    • 语法问题
    • 算符优先级问题
    • 分号吞噬问题
    • 宏参数重复调用
    • 对自身的递归调用
    • 宏参数预处理
    • 与自加运算的结合

总结一下C语言中宏的一些特殊用法和几个容易踩的坑。由于本文主要参考GCC文档,某些细节(如宏参数中的空格是否处理之类)在别的编译器可能有细微差别,请参考相应文档。

一、宏基础

宏仅仅是在 C 预处理阶段的一种文本替换工具,编译完之后对二进制代码不可见(仅用于文本替换,比如#define N 10000N出现的位置在宏处理阶段,即被原样替换为10000,二进制代码中不会出现任何关于N的内容)。基本用法如下:

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的利用和指令预测,速度快。宏看起来像函数,但不会招致函数调用(function call)带来的额外开销。

  • 缺点是可执行代码体积大;

#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))

y = MIN(1, 2);会被替换为y = ((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);

2. ## ⇒ 连接(Concatenation)

在宏体中,如果宏体所在的标识符中有##,那么在宏体拓展的时候,宏参数会直接替换到标识符中,如:

#define COMMAND(NAME) {#NAME, NAME ##_command}

struct command
{
    char* name;
    void (*func)(void);
};

在宏拓展的时候,

struct command cmds[] = 
{
    COMMAND(quit);
    COMMAND(help);
    ... 
}

会被拓展为:

struct command cmds = 
{
    {'quit', quit_command},
    {'help', help_command},
    ...
};

二、几个坑

1. 语法问题

由于是纯文本替换,C 预处理器(preprocessor)不对宏体做任何语法检查,像缺个括号、少个分号神马的预处理器是不管的。这里要格外小心,由此可能引出各种奇葩的问题,一下还很难找到根源。

2. 算符优先级问题

不仅宏体是纯文本替换,宏参数也是纯文本替换。有以下一段简单的宏,实现乘法:

#define MUL(X, Y) x*y

MUL(1, 2)自然没有问题,会正常展开为1*2。有问题的是这种表达是MUL(1+2, 3),展开后成了1+2*3,显然优先级出错。

在宏体中,给引用的参数加个括号就能避免这个问题,

#define MUL(x, y) ((X)*(Y))

其实这个问题和下面要说到的某些问题都属于由于纯文本替换而导致的语义破坏问题,要格外小心。

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逻辑块就结束了,所以编译器发现与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)));,可以看到当((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未被展开成X_1024,因为宏体中有##,宏参数不经展开直接代入宏体。

  • XAFTERX(BUFSIZE)会被展开成X_1024,因为XAFTERX(x)的宏体是AFTERX(x),并没有###,所以BUFSIZE在代入之前会被完全展开成1024,然后才代入宏体,变成X_1024

7. 与自加运算的结合

纵使有时我们为所有实参都加上小括号,如下:

#define MAX(x, y) ((x)>(y)?(x):(y))

仍然会有一些奇异的事情发生:

int a = 5, b = 0;
MAX(++a, b);                    // a 被累加两次,变为7
MAX(++a, b+10);                 // a 被累加1次,变为6

a的递增次数竟然取决于“它被拿来与谁相比较”。

你可能感兴趣的:(C语言宏的特殊用法和几个坑)