C语言宏(macro)小技巧

发布时间 2023-06-10 19:29:26作者: NOSAE

字符串化运算符(stringizing operator)

运算符 # 在宏中会将实参直接转化为字符串literal,也就是字符串常量,举个简单的例子:

#define arg2str(p) #p
puts(arg2str(666)); // 宏展开后变成如下
puts("666")

简单却有用的使用场景,计算一个表达式,将表达式和结果都一起输出:

#define calc(exp) printf( #exp " = %f ", exp )
calc( 4 * atan(1.0)); // 宏展开后如下
printf( "4 * atan(1.0) = %f ", 4 * atan(1.0));

记号粘贴运算符(token-pasting operator)

运算符 ## 在宏中会将两个符号连接起来,因此我更喜欢把它叫做连接符,举个简单的例子:

#define text_1 "hello"
#define myconcat() text_ ## 1
puts(myconcat()); // 宏展开如下
puts("hello")

可以看到就是将text_和1作为符号连接起来。再举一个带参数的例子,一般带参数的才有使用场景

#define text_1 "hello"
#define text_2 "world"
#define myconcat2(p) text_ ## p
puts(myconcat2(1));
puts(myconcat2(2));
// 宏展开分别为如下
puts("hello");
puts("world");

至此,我们还需要注意一个关键点,无论是什么样的宏,传入宏的实参只会被当作一个符号,而不会被识别成变量、宏或是别的东西并进行展开,可能不好理解,举个例子说明:

#define EXPAND 1
#define text_1 "hello"
#define myconcat(p) text_ ## p
puts(myconcat(EXPAND));
// 宏展开后报错:未定义标识符text_EXPAND

可以看到当我们将EXPAND作为实参传进去时,EXPAND只是被简单连接在text_的后面,而不是先展开成1再连接。类似地,如果实参EXPAND是一个变量而不是宏,比如

int EXPAND = 1;

那么相信你更可以确定实参只是一个符号而不是被展开成变量存储的值,因为宏展开是在预处理阶段(甚至还没到编译阶段),还没有变量的概念,自然也就无法“展开”这个变量

再进一步地,看一下这个例子

#define EXPAND 1
#define text_1 "hello"
#define myconcat(p) text_ ## p
#define myconcat2(p) myconcat(p)
puts(myconcat2(EXPAND)); // 宏展开如下
puts("hello");

可以看到我们多封装了一层宏,这样就不会报错了。至此,我们可以总结出,实参只是作为一个符号,但形参是根据实际来决定是否展开的,例如myconcat2中,EXPAND传进去后,形参p并没有进行 # 或 ##运算,因此被展开为1并传入myconcat,最终成功得到text_1

二选一选择器宏(mux)

有时候我们要根据宏是否存在从而选择编译哪部分的代码,比如

#define COND 1

#ifdef COND
	printf("hello");
#else
	printf("world");
#endif

上面的代码在预处理过后只保留了第一个printf,这个不必多说

printf("hello");

但是这种使用也有他的局限性,比如我们要根据宏是否定义从而来决定一个变量的值

#define COND 1

#ifdef COND
	int var = val1;
#else
	int var = val2;
#endif

可以看到这种实现方式很麻烦和啰嗦,如果有很多地方都有这种判断,而且不能一次性写在一起的话,代码中就会充斥大量的ifdef else endif。下面要介绍的宏二选一选择器,是我写某个项目时学到的

先看看怎么用这个宏简化上面的var赋值代码

#define COND 1
int var = MUXDEF(COND, val1, val2); // 展开后如下
int var = val1;

MUXDEF就是我们的主角,二选一选择器宏,传入的参数就不多说了,对比上面的“繁琐版”就知道是什么意思,可以看到这个宏实现了make your life easier(

除了二选一选择值,你也可以用来分支代码段

#define COND 1
MUXDEF(COND, printf("hello"), (puts("world1"), puts("world2"))); // 展开后如下
printf("hello");

使用十分简单,但实现却十分巧妙,下面看看这个宏怎么实现的:

#define concat(x, y) x ## y

#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)

#define __P_DEF_1  X,

#define MUXDEF(macro, X, Y)  MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)

下面的宏依赖于上面的宏,我们从上往下看

  • concat只是用来语义化连接操作的
  • CHOOSE2nd很明显,传入两个或以上的参数,并替换为第二个参数
  • MUX_WITH_COMMA是核心,十分重要,但强行解释没意思,下面用例子来解释
  • MUX_MACRO_PROPERTY也很明显,不解释
  • __P_DEF_1MUX_WITH_COMMA结合生效,下面用例子解释
  • MUXDEF我们的主角

直接看例子吧,然后通过一步步宏展开来看,首先是COND=1的情况

#define COND 1
int var = MUXDEF(COND, 1, 2); // 宏展开如下
int var = 1;

将第二行的宏一步步展开(下一行是当前行的展开)

MUXDEF(COND, 1, 2);
MUX_MACRO_PROPERTY(__P_DEF_, COND, 1, 2);
MUX_MACRO_PROPERTY(__P_DEF_, 1, 1, 2);
MUX_WITH_COMMA(concat(__P_DEF_, 1), 1, 2);
MUX_WITH_COMMA(__P_DEF_1, 1, 2);
CHOOSE2nd(__P_DEF_1 1, 2);
CHOOSE2nd(X, 1, 2);
1

然后再看一下COND没有定义的情况

int var = MUXDEF(COND, 1, 2); // 宏展开如下
int var = 1;
MUXDEF(COND, 1, 2);
MUX_MACRO_PROPERTY(__P_DEF_, COND, 1, 2);
// 与上一种情况对比,COND没被展开,因为没有定义COND这个宏
MUX_WITH_COMMA(concat(__P_DEF_, COND), 1, 2);
MUX_WITH_COMMA(__P_DEF_COND, 1, 2);
CHOOSE2nd(__P_DEF_COND 1, 2);
2

通过逐步展开宏事情已经一目了然,即通过是否加上逗号,使得CHOOSE2nd中所选择的第二个参数,变成我们第二个或者第三个实参。注意到需要与__P_DEF_1这个宏的名字匹配,因此我们的COND只能定义成1而不能是别的,否则就会匹配失败,当然,你也可以写成__P_DEF_y,然后#define COND y就可以