宏的拓展用法总结

对于C语言的初学者,可能只是简单的用宏去定义一些变量或者简单函数。然而宏的功能远远不是这么简单的,在一些比较著名的开源软件中,都能看到大量的宏定义。本文结合GCC文档,对宏的一些扩展用法进行了总结,由于个人水平原因,可能并不是十分全面,欢迎各路大神补充。

部分样例来自于GCC文档: https://gcc.gnu.org/onlinedocs/cpp/Macros.html#Macros

# & ##

#是将参数转换成字符串,在参数两边自动加上双引号。

1
2
3
#define typestr(x) #x

typestr(int) ==>"int"

##用于连接串

1
2
3
#define connect(x,y) func_x##_##y

connect(hello, world) ==> func_hello_world

这两个宏经常用来生成代码,比如我们要定义一个命令列表。

1
2
3
4
5
6
7
8
struct command {
char *name;
void (*func)(void*);
};
struct command commands[] = {
{"quit", quit_command},
{"help", help_command}
}

列表的名字和回调函数之间有一定的联系,我们可以用宏做如下替换。

1
2
3
4
5
#define COMMAND(NAME) {#NAME, NAME##_command}
struct command commands[] = {
COMMAND(quit),
COMMAND(help)
}

这里还有一个小技巧,就是用宏#来查看宏生成的结果,这在测试一些复杂宏时十分有用。xstr宏的目的是先让参数展开,之后再通过str宏转化成字符串。

1
2
3
4
5
6
7
8
#define xstr(x) str(x)
#define str(x) #x
#define foo 4

xstr(foo) => xstr(4)
=> str(4)
=> "4"
str(foo) => "foo"

可变参数列表

宏可以接收可变参数列表,我们可以用这个特性来封装一个DEBUG宏用于输出调试信息。

1
2
3
4
5
#define DEBUG(fmt, ...) printf("DEBUG: "fmt"\n", __VA_ARGS__)

int x = 10;
DEBUG("%d", x)
===> printf("DEBUG: %d\n", x)

但是这个宏是有一些问题的,下面的代码会因为宏展开后多一个逗号而无法通过编译。

1
DEBUG("hello") ===> printf("hello",)

解决的办法就是在__VA_ARGS__前加上##,这两个#会在__VA_ARGS__为空时消除前面的逗号。

1
2
#define DEBUG(fmt, ...) printf("DEBUG: "fmt"\n", ##__VA_ARGS__)
DEBUG("hello") ===> printf("hello")

代码块替换

在程序中,我们通常用{}来包含一个代码块,但在宏中这样使用会出现一些问题,比如如下一个宏

1
2
3
4
5
#define SKIP_SPACES(p, limit)  \
{ char *lim = (limit); \
while (p < lim) { \
if (*p++ != ' ') { \
p--; break; }}}

在如下代码中展开,就会因为在else前多了一个分号而无法通过编译。

1
2
3
if (*p != 0)
SKIP_SPACES (p, lim);
else ...

解决方法就是用do{}while(0)来包含代码块,这里的分号就不会产生编译错误。

1
2
3
4
5
6
#define SKIP_SPACES(p, limit)     \
do { char *lim = (limit); \
while (p < lim) { \
if (*p++ != ' ') { \
p--; break; }}} \
while (0)

返回值

有时候一个宏包含若干条语句,而我们又希望这个宏最后能够返回一个值,这时就可以用({..})来包含一段代码,这段代码的最后一行会作为整个代码段的值。下面使用求较小值的宏来举例。
很多C程序中都会使用下面这个宏来得到最小值

1
#define min(X, Y)  ((X) < (Y) ? (X) : (Y))

这个宏的如果使用不当,会严重影响程序效率,比如下面这种情况就会两次运行foo函数。

1
2
next = min (x + y, foo (z)); ==>
next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));

而正确的写法就是使用代码块先计算出X和Y的值,最后对计算出的值进行比较。宏中typeof是GUN C的一个扩展,用于得到参数的类型。

1
2
3
4
5
6
7
8
9
10
11
#define min(X, Y)               \
({ typeof(X) x_ = (X); \
typeof(Y) y_ = (Y); \
(x_ < y_) ? x_ : y_; })

//这里假设x,y以及foo(z)的返回值都是int类型
min (x + y, foo (z)) ===>
({ int x_ = x + y;
int y_ = foo(z);
(x_ < y_) ? x_ : y_;
})

这个宏的最后一条语句就是用于返回整个代码块的执行结果。

嵌套定义

可以将宏名作为另一个宏的参数,然后在宏中调用,类似于回调函数的效果。

1
2
3
4
5
6
#define TWICE(x) ((x) * 2)
#define ADDONE(x) ((x) + 1)
#define NEWSIZE(x, func_expand) (x = func_expand(x))

NEWSIZE(x, TWICE) ==> (x = ((x) * 2))
NEWSIZE(x, ADDONE) ==> (x = ((x) + 1))

内置宏

C中提供了一些内置的宏,GNU C中也提供了很多十分有用的内置宏,需要注意并不是所有的编译器都支持这些宏。这里列举几个比较有用的内置宏,更多的宏可以在GNU C文档中找到。

1
2
3
4
5
__FILE__ 当前所在文件
__LINE__ 当前代码所在行
__DATA__ 当前日期(格式 Feb 12 1996)
__TIME__ 当前时间(格式 23:59:01)
__COUNTER__ 累加器,从0开始,每次调用后加1

文章目录
  1. 1. # & ##
  2. 2. 可变参数列表
  3. 3. 代码块替换
  4. 4. 返回值
  5. 5. 嵌套定义
  6. 6. 内置宏