C/C++宏的奇技淫巧

5,710 阅读5分钟

宏的使用是一个非常高的技巧,使用好了可以节省大量的代码,而使用不好会出现意想不到的bug,本文 浅尝辄止地谈一下宏的使用

在编译之前,预处理器会分析源文件,若遇到宏名称的时候,预处理器就会展开宏,即会用定义的文本来取代宏名称。

宏的定义

无参数的宏

#define 宏名称 替换文本

有参数的宏

当预处理器展开这类宏时,它先使用调用宏时指定的实际参数(简称“实参”)取代替换文本中对应的形参。带有形参的宏通常也称为类函数宏

#define 宏名称( [形参列表] ) 替换文本
#define 宏名称( [形参列表 ,] ... ) 替换文本

C99 允许在调用宏的时候,宏的实参列表可以为空。在这种情况下,对应的替换文本中的形参不会被取代;也就是说,替换文本会删除该形参。然而,并非所有的编译器都支持这种“空实参”的做法。

如果调用时的实参也包含宏,在正常情况下会先对它进行展开,然后才把该实参取代替换文本中的形参。对于替换文本中的形参是 # 或 ## 运算符操作数的情况,处理方式会有所不同。

宏中的可选参数__VA_ARGS__

C99 标准允许定义有省略号的宏,省略号必须放在参数列表的后面,以表示可选参数。你可以用可选参数来调用这类宏。 当调用有可选参数的宏时,预处理器会将所有可选参数连同分隔它们的逗号打包在一起作为一个参数。在替换文本中,标识符__VA_ARGS__对应一组前述打包的可选参数。标识符__VA_ARGS__只能用在宏定义时的替换文本中。 __VA_ARGS__ 的行为和其他宏参数一样,唯一不同的是,它会被调用时所用的参数列表中剩下的所有参数取代,而不是仅仅被一个参数取代。下面是一个可选参数宏的示例:

// 假设我们有一个已打开的日志文件,准备采用文件指针fp_log对其进行写入
#define printLog(...) fprintf( fp_log, __VA_ARGS__ )
// 使用宏printLog
printLog( "%s: intVar = %d\n", __func__, intVar );

预处理器把最后一行的宏调用替换成下面的一行代码:
fprintf( fp_log, "%s: intVar = %d\n", __func__, intVar );

预定义的标识符__func__可以在任一函数中使用,该标识符是表示当前函数名的字符串。

宏中的运算符

字符串化运算符#

它会把宏调用时的实参转换为字符串。#的操作数必须是宏替换文本中的形参。当形参名称出现在替换文本中,并且具有前缀 # 字符时,预处理器会把与该形参对应的实参放到一对双引号中,形成一个字符串字面量。

实参中的所有字符本身维持不变,但下面几种情况是例外:

  1. 在实参各记号之间如果存在有空白符序列,都会被替换成一个空格符。
  2. 实参中每个双引号(")的前面都会添加一个反斜线(\)。
  3. 实参中字符常量、字符串字面量中的每个反斜线前面,也会添加一个反斜线。但如果该反斜线本身就是通用字符名的一部分,则不会再在其前面添加反斜线。
#define printDBL( exp ) printf( #exp " = %f ", exp )
printDBL( 4 * atan(1.0));           // atan()在math.h中定义
上面的最后一行代码是宏调用,展开形式如下所示:
printf( "4 * atan(1.0)" " = %f ", 4 * atan(1.0));
因为编译器会合并紧邻的字符串字面量,上述代码等效为:
printf( "4 * atan(1.0) = %f ", 4 * atan(1.0));

#define showArgs(...) puts(#__VA_ARGS__)
showArgs( one\n,       "2\n", three );
预处理器使用下面的文本来替换该宏:
puts("one\n, \"2\\n\", three");

记号粘贴运算符##

运算符是一个二元运算符,可以出现在所有宏的替换文本中。该运算符会把左、右操作数结合在一起,作为一个记号。如果结果文本中还包含有宏名称,则预处理器会继续进行宏替换。出现在##运算符前后的空白符连同##运算符本身一起被删除。

#define TEXT_A "Hello, world!"
#define msg(x) puts( TEXT_ ## x )
msg(A);
无论标识符 A 是否定义为一个宏名称,预处理器会先将形参 x 替换成实参 A,然后进行记号粘贴。当这两个步骤做完后,结果如下:
puts( TEXT_A );
因为 TEXT_A 是一个宏名称,后续的宏替换会生成下面的语句:
puts( "Hello, world!" );

宏内使用宏

需要注意的是凡宏定义里有用'#''##'的地方宏参数是不会再展开

#define A          (2) 
#define STR(s)     #s 
#define CONS(a,b)  int(a##e##b) 

printf("int max: %s\n",  STR(INT_MAX));    // INT_MAX #include<climits> 
这行会被展开为: 
printf("int max: %s\n", "INT_MAX"); 

printf("%s\n", CONS(A, A));               // compile error  
这一行则是: 
printf("%s\n", int(AeA)); 

然而解决这个问题的方法很简单. 加多一层中间转换宏。加这层宏的用意是把所有宏的参数在这层里全部展开, 那么在转换宏里的那一个宏(_STR)就能得到正确的宏参数.

#define A           (2) 
#define _STR(s)     #s 
#define STR(s)      _STR(s)          // 转换宏 
#define _CONS(a,b)  int(a##e##b) 
#define CONS(a,b)   _CONS(a,b)       // 转换宏 

printf("int max: %s\n", STR(INT_MAX));          // INT_MAX,int型的最大值,为一个变量 #include<climits> 
输出为: int max: 0x7fffffff 
STR(INT_MAX) -->  _STR(0x7fffffff) 然后再转换成字符串; 

printf("%d\n", CONS(A, A)); 
输出为:200 
CONS(A, A)  -->  _CONS((2), (2))  --> int((2)e(2)) 

宏的作用域和重新定义

你无法再次使用 #define 命令重新定义一个已经被定义为宏的标识符,除非重新定义所使用的替换文本与已经被定义的替换文本完全相同。 如果想改变一个宏的内容,必须首先使用下面的命令取消现在的定义:

#undef 宏名称

当某个宏首次遇到它的 #undef 命令时,它的作用域就会结束。如果没有关于该宏的 #undef 命令,那么它的作用域在该翻译单元结束时终止。

参考文献

C语言宏的定义和宏的使用方法
C语言宏中"#"和"##"的用法
C语言宏高级用法