前言
本文介绍一些 C/C++ 中宏的正常用法。 以及一些你绝对不会想让你家孩子知道的令人惊叹的技术。
1 相关知识
宏是在编译之前由预处理器处理的替换规则, 仅进行字符串替换, 并没有值的感念. 宏有两种风格, 一种和对象类似:
#define identifier replacement-list
这里 identifier 是宏的名字, replacement-list 是一个或多个 tokens 的序列。在后面的程序中所有出现 identifier 的地方都会被展开为 replacement-list 。
另一种是函数风格的宏,它就好比“预处理期的元函数”:
#define identifier(a1, a2, ... an) replacement-list
这里,每一个ai 都代表一个宏形参(parameter)的名字。 如果后面的程序中用到了该宏,并给出了适当的实参(argument), 那么它将被扩展为 replacement-list ——其中每次出现宏形 参的地方都会被替换为用户给出的宏实参。
如果学习过 C 语言,我们通常会像这样使用宏:
int bigarray[MAXN];
这样在预处理期, MAXN会被替换成10001, 也就是说上面的代码在预处理之后变成了
int bigarray[10001];
使用 gcc的 -E 选项可以查看预处理结果, 也有更好的方式, 将在后面介绍.
函数形式的宏比较常用:
#ifndef min
# define min(x,y) ({
\
typeof(x) _x = (x); \
typeof(y) _y = (y); \
(void) (&_x == &_y); \
_x < _y ? _x : _y; })
#endif
#ifndef max
# define max(x,y) ({
\
typeof(x) _x = (x); \
typeof(y) _y = (y); \
(void) (&_x == &_y); \
_x > _y ? _x : _y; })
#endif
#ifndef swap
#define swap(x,y) do {
\
typeof(x) __tmp = (x); \
(x) = (y); (y) = __tmp; \
} while(0)
#endif
swap 宏中do … while(0) 的作用是将几条语句合成一条. 如果没有 do {…} while(0), 下面这句话就会有问题:
if (x > y)
swap(x, y);
如果没有 do {…} while (0), 则只有 typeof(x) __tmp = (x); 在 if 的条件为真下执行, 其它会在任何条件下执行.
在GCC环境中,如果对效率有较高的要求, 可能会需要下面这两个宏:
#ifndef likely
# define likely(x) __builtin_expect(!!(x), 1)
#endif
#ifndef unlikely
# define unlikely(x) __builtin_expect(!!(x), 0)
#endif
ikely(x) 告诉编译器 x 为真的概率较大, 编译器据此可以优化跳转预测.
比较常用的宏还有下面这个:
#ifndef container_of
/**
* container_of - cast a member of a structure out to
* the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
# define container_of(ptr, type, member) ({
\
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
它的用处注释里都已经说清楚了。
2 宏的规则
如果你忘记了, 那么函数形式的宏是这个样子的:
#define identifier(a1, a2, ... an) replacement-list
identifier(…) 替换为 replacement-list, 其中的实參将按照宏的定义放置到对应位置.
如果实參是宏, 则先展开 identifier 的实參, 再展开当前宏定义的 identifier(…), 除非遇到规则3.
replacement-list 中形如 # a1 的被替换为字符串 a1.
replacement-list 中的 name## a1 将被预处理器连接 namea1, 其中 a1 为实參.
replacement-list 中出现当前定义的 identifier, 则停止展开.
3 打印宏展开后的样子
仔细观察宏的规则, 很容易理解将宏展开之后的样子转换为字符串以打印出来的宏:
#define PP_STRINGIZE(text) PP_STRINGIZE_I(text)
#define PP_STRINGIZE_I(text) #text
此后就可以使用这个宏来观察一个宏展开之后的样子了:
puts(PP_STRINGIZE(min(1, 2)));