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
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
。
C语言宏的特殊用法和几个坑(转)的更多相关文章
- 【转】C语言宏定义的几个坑和特殊用法
总结一下C语言中宏的一些特殊用法和几个容易踩的坑.由于本文主要参考GCC文档,某些细节(如宏参数中的空格是否处理之类)在别的编译器可能有细微差别,请参考相应文档. 宏基础 宏仅仅是在C预处理阶段的一种 ...
- C语言宏的使用
使用条件宏进行条件编译 譬如,对于同一份代码,我想编译出两个不同的版本,在其中一个版本中去掉某一部分功能, 这时可以通过条件宏判断是否编译,例: 如果不使用条件宏进行控制,想编译两个不同版本的程序,就 ...
- C语言 宏/macor/#define/
C语言 宏/macor/#define 高级技巧 1.在进行调试的时候,需要进行打印/PRINT,可以通过define进行自定义.例如,自己最常用的DEBUG_PRINT() #define DEBU ...
- C语言宏的高级应用
原文:C语言宏的高级应用 关于#和##在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号.比如 ...
- C 语言宏定义
C 语言宏定义1.例子如下: #define PRINT_STR(s) printf("%s",s.c_str()) string str = "abcd"; ...
- 将C语言宏定义数值转换成字符串!
将C语言宏定义转换成字符串! 摘自:https://blog.csdn.net/happen23/article/details/50602667 2016年01月28日 19:15:47 六个九十度 ...
- C语言宏应用-------#define STR(X) #X
C语言宏应用-------#define STR(X) #X #:会把参数转换为字符串 #define STR(x) #x #define MAX 100 STR(MAX) 会被扩展成" ...
- C语言 宏定义之可变参数
可变参数宏定义 C99编译器标准允许你可以定义可变参数宏(variadic macros),这样你就可以使用拥有可以变化的参数表的宏.可变参数宏就像下面这个样子: #define dbgprint(. ...
- C语言宏定义时#(井号)和##(双井号)的用法1
#在英语里面叫做 pound 在C语言的宏定义中,一个#表示字符串化:两个#代表concatenate 举例如下: #include <iostream> void quit_comman ...
随机推荐
- CF R303 div2 C. Woodcutters
C. Woodcutters time limit per test 1 second memory limit per test 256 megabytes input standard input ...
- ubuntu使用crontab
crond服务通常被放在/etc/init.d/crond , 这样就可以在系统启动后自动启动crond服务. linux中的用户使用crontab命令来配置cron任务. crontab在/etc目 ...
- UIKit封装的系统动画
简介 在UIKit中,对UIView封装了很多类方法来进行简单的动画实现,在动画过程中,通过对属性值的修改来完成一系列的效果. 在IOS4以前,主要通过 + beginAnimation + setA ...
- 高性能JSON库---FastJson(阿里巴巴)
1.FastJSON简单介绍 Fastjson是一个Java语言编写的高性能功能完好的JSON库. 它採用一种"假定有序高速匹配"的算法,把JSON Parse的性能提升到极致,是 ...
- 上传多张图片用Session临时存储
DataTable dtImages = new DataTable(); string filepath = FileUpload1.PostedFile.FileName; //检查是否有文件要上 ...
- ASP.NET之电子商务系统开发-4(二级分类)
一.前言 继上次的订单,这是第四篇.记录一下分类和筛选.这功能是最后做的,因为我完全不懂其原理.后来通过同学的指导(一位很有天赋的同学,比我牛逼一个层次,同样是高三.:D),终于也是完成了.在写这篇博 ...
- BZOJ 1207: [HNOI2004]打鼹鼠( dp )
dp.. dp[ i ] = max( dp[ j ] + 1 ) ------------------------------------------------------------------ ...
- 5.4.1 RegExp实例属性
RegExp的每个实例都具有下列属性,通过这些属性可以取得有关模式的各种信息. 1.global:布尔值,表示是否设置了 g 标志. 2.ignoreCase:布尔值,表示 ...
- Linux中fork()函数详解(转)
一.fork入门知识 一个进程,包括代码.数据和分配给进程的资源.fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同, ...
- Linux第三方源
由于版权等各方面原因,很多时候在基础安装完Linux后,满多软件并不包含在yum(对于Ubuntu可能是apt-get)源中. 因此可以去下载第三方源,安装下载第三方软件.当然,如果习惯了源代码编译安 ...