原文请见 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语言宏的特殊用法和几个坑相关推荐

  1. C语言宏定义的几个坑和特殊用法

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

  2. c语言中#39;xd#39;代表什么,关于C语言宏定义的技巧:#39;##39;和#39;###39;

    关于C语言宏定义的技巧:'#'和'##' '#'和'##' '#'和'##'是两个预处理运算符,只能在预处理的过程中使用.在带参数的宏定义中, '#'运算符后面应该跟一个参数,预处理器会把这个参数转换 ...

  3. 转载--c语言宏定义(1)

    作者:独舞风 链接:c语言宏定义(1) 1.为什么要有宏定义? 代码中某个特定数值需要参与运算,而且该数值作用于多个地方,当需要对该数值进行修改时,希望只改动一个地方就能实现该数值的全部更新:即便某个 ...

  4. C/C++语言宏定义##连接符和符#的使用

    C/C++语言宏定义##连接符和符#的使用     [尊重原创,转载请注明出处]http://blog.csdn.net/guyuealian/article/details/53113187 (一) ...

  5. 错误: 非法的表达式开始_虽然这两个C语言宏定义很简单,但是能在程序运行前找到错误代码...

    今天翻看 Linux 内核源代码时,发现两行非常有意思的C语言代码,如下: #define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))# ...

  6. c语言 宏定义 去除宏定义_如何检查是否在C中定义了宏?

    c语言 宏定义 去除宏定义 To check whether a Macro is defined or not in C language – we use #ifdef preprocessor ...

  7. c语言宏函数怎么传递宏参数_C语言中的宏参数评估

    c语言宏函数怎么传递宏参数 We can define a function like Macro, in which we can pass the arguments. When a Macro ...

  8. c语言宏定义技巧和用法,关于C语言宏定义的技巧:'#'和'##'

    关于C语言宏定义的技巧:'#'和'##' '#'和'##' '#'和'##'是两个预处理运算符,只能在预处理的过程中使用.在带参数的宏定义中, '#'运算符后面应该跟一个参数,预处理器会把这个参数转换 ...

  9. c语言 宏do while,关于C语言宏定义 使用do{ xxxx }while()

    暂时感觉像是由于":"的原因,关于使用习惯方面的问题!! 下面是copy的: 这样的宏见过么: Cpp代码 #define FOO(x) do {\ some_code_line_ ...

最新文章

  1. Postfix邮件服务搭建
  2. 【Java 线程的深入研究3】最简单实例说明wait、notify、notifyAll的使用方法
  3. salad--8||9
  4. 2015-03-17 current note creation logic in my task
  5. 小弟带你走进VUE中input最大值设置出现的问题以及黑科技解决方案
  6. Java7并发编程指南——第二章:线程同步基础
  7. C语言关系运算符及其表达式
  8. Spring-core-AnnotationMetadata接口
  9. 题解 CF1027D 【Mouse Hunt】
  10. 阿里云主机的公网带宽和私网带宽的介绍
  11. 【五年】Java打怪升级之路
  12. 线程的6种状态(NEW,RUNNABLE,BLOCKED,WAITING,TINED_WATING,TEMINATE)
  13. 【CZY选讲·Triangle】
  14. Camera2 开发问题记录
  15. Linux篇19多线程第三部分
  16. 数字IC入门工具大全之 英特尔 Quartus Prime是什么?三个版本有什么区别
  17. 小米云网站服务器错误代码,小米健康云开放平台iOS SDK使用指南
  18. webview与Chrome版本匹配
  19. 好看又好用的彩色口罩,舒适佩戴安全可靠
  20. A - 不定方程求解

热门文章

  1. LeetCode 368. 最大整除子集(动态规划)
  2. Win10 64位+VS2015+OpenCV3.4.2重编译
  3. QT5之MYSQL操作
  4. 初学java andriod 软件安装与配置问题
  5. chinese-ocr自然场景下不定长文字识别(ctpn + densenet)
  6. android 按钮旋转等待,android高分段进阶攻略(3)旋转等待UI界面设计
  7. JAVA MIDP_Java MIDP2.1和JAVA MIDP2.0的本质区别是什么啊?
  8. b+tree索引在MyIsam和InnoDB的不同实现方式
  9. 避免unicode字符被截断的方法
  10. 在Pandas中直接加载MongoDB的数据