字符串化运算符#
一元运算符 # 常称为字符串化运算符(stringify operator 或 stringizing operator),因为它会把宏调用时的实参转换为字符串。# 的操作数必须是宏替换文本中的形参。当形参名称出现在替换文本中,并且具有前缀 # 字符时,预处理器会把与该形参对应的实参放到一对双引号中,形成一个字符串字面量。
实参中的所有字符本身维持不变,但下面几种情况是例外:
(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));
该语句会生成下列文字并在控制台输出:
4 * atan(1.0) = 3.141593
在下面的示例中,调用宏 showArgs 以演示 # 运算符如何修改宏实参中空白符、双引号,以及反斜线:
#define showArgs(...) puts(#__VA_ARGS__)
showArgs( one\n, "2\n", three );
预处理器使用下面的文本来替换该宏:
puts("one\n, \"2\\n\", three");
该语句生成下面的输出:
one
, "2\n", three
记号粘贴运算符##
运算符是一个二元运算符,可以出现在所有宏的替换文本中。该运算符会把左、右操作数结合在一起,作为一个记号,因此,它常常被称为记号粘贴运算符(token-pasting operator)。如果结果文本中还包含有宏名称,则预处理器会继续进行宏替换。出现在 ## 运算符前后的空白符连同 ## 运算符本身一起被删除。
通常,使用 ## 运算符时,至少有一个操作数是宏的形参。在这种情况下,实参值会先替换形参,然后等记号粘贴完成后,才进行宏展开。如下例所示:
#define TEXT_A "Hello, world!"
#define msg(x) puts( TEXT_ ## x )
msg(A);
无论标识符 A 是否定义为一个宏名称,预处理器会先将形参 x 替换成实参 A,然后进行记号粘贴。当这两个步骤做完后,结果如下:
puts( TEXT_A );
现在,因为 TEXT_A 是一个宏名称,后续的宏替换会生成下面的语句:
puts( "Hello, world!" );
如果宏的形参是 ## 运算符的操作数,并且在某次宏调用时,并没有为该形参准备对应的实参,那么预处理使用占位符(placeholder)表示该形参被空字符串取代。把一个占位符和任何记号进行记号粘贴操作的结果还是原来的记号。如果对两个占位符进行记号粘贴操作,则得到一个占位符。
当所有的记号粘贴运算都做完后,预处理器会删除所有剩下的占位符。下面是一个示例,调用宏时传入空的实参:
msg();
这个调用会被展开为如下所示的代码:
puts( TEXT_ );
如果TEXT_不是一个字符串类型的标识符,编译器会生成一个错误信息。
字符串化运算符和记号粘贴运算符并没有固定的运算次序。如果需要采取特定的运算次序,可以将一个宏分解为多个宏。