文档章节

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

shzwork
 shzwork
发布于 03/14 09:30
字数 1515
阅读 12
收藏 1

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

总结一下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

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

-EOF-

本文转载自:https://www.cnblogs.com/roadmap99/p/6868930.html

shzwork

shzwork

粉丝 15
博文 898
码字总数 18759
作品 0
厦门
私信 提问
神奇的C语言,这才是C语言大牛操作,作为面试题,怕是秒杀众人

当然下面列出来的几点都是C的基础用法,只不过是这些用法可能平时不会被注意。所以很多东西第一次看到的时候,可能会觉得很怪异,但是细细想想就能很好的理解,也就能更好的清楚C语言的一些特...

C语言叶子编程
2018/07/27
0
0
哈, 抽空上来喷句代码设计。。。。

看了几个c语言的帖子,讨论一些特殊的逻辑描述。真心想不出这种坑怎么还在继续挖。为了指明这是坑,我喷一喷。仍然那句话,我野鬼,你权当是个喷壶,无所谓。哈。无非看到坑指出来,你爱跳不...

中山野鬼
2014/10/28
1K
15
总结几个C语言中的“坑”(一)

近来被C语言中的各种“坑”坑惨了,现将其总结如下,防止以后再被坑: 1、带参数的宏展开顺序 #include b a define h(a) g(a) int main(void){ }运行结果: 12 f(1,2) 浅析: 本题中的#运算符可...

Jung_zhang
2015/05/27
0
0
【转】编译器 cc、gcc、g++、CC 的区别

【转】编译器 cc、gcc、g++、CC 的区别 cstriker1407的笔记本2017-12-123 阅读 C编程C++linux 本文转自【 https://www.cnblogs.com/52php/p/5681725.html 】 gcc 是GNU Compiler Collection......

cstriker1407的笔记本
2017/12/12
0
0
Linux下__attribute__((aligned(n)))的使用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fengbingchun/article/details/81321419 关键字attribute允许你在定义struct、union、变量等类型时指定特殊属...

fengbingchun
2018/07/31
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Java8新特性,语法学习

package com.example.demo.java8;import com.example.demo.domin.User;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.stream.Co......

Gx_ww
6分钟前
4
0
type()和isinstance()之间有什么区别?

这两个代码片段之间有什么区别? 使用type() : import typesif type(a) is types.DictType: do_something()if type(b) in types.StringTypes: do_something_else() 使用isinsta......

javail
6分钟前
4
0
Flink-本地设置教程

设置:下载并启动Flink 启动本地Flink群集 阅读代码 运行示例 下一步 通过几个简单的步骤即可启动并运行Flink示例程序。 设置:下载并启动Flink Flink在Linux,Mac OS X和Windows上运行。为了...

sunwuhan
9分钟前
2
0
RPA是什么?RPA发展极简史

眼下,RPA(机器人流程自动化)技术爆火于科技圈与投资界,并持续引发新一轮的数字化变革。 与过去几年间的很多技术投资热点不同,如今RPA技术被资本方和市场方广为看好,原因有二:一是RPA...

UiBot
10分钟前
3
0
超级账本Fabric的交易背书过程解读

Hyperledger Fabric和其他许多区块链的关键区别之一,就在于Fabric区块链的交易执行过程:Fabric交易需要首先通过节点的背书,然后再进行交易排序,最后才利用有序交易进行账本的更新。本文将...

区块链教程
12分钟前
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部