预处理指令(八)
3、#运算符
如果替换列表中某个形参紧跟在#预处理标记后,两者将替换为单个字符串字面量预处理标记,该预处理标记包含对应实参的预处理标记序列拼写(不包含占位标记);因此#运算符也称为字符串化运算符。
#define STRINGIZE(x) # x
宏定义中#运算符和实参间的空格是可选的。
#define STRINGIZE(x) # x //等价于#define STRINGIZE(x) #x。
#运算符仅用于函数式宏;函数式宏替换列表中每个#预处理标记后应紧跟一个形参作为替换列表中的下一个预处理标记。
#define PRINT(x, y) printf(#x " " #y) PRINT(Hello, World!); //输出Hello World!。
作为#运算符的操作数,实参不会先进行宏展开。
#define TWO 2 #define STRINGIZE(x) # x printf("%s\n", STRINGIZE(TWO)); //输出TWO。
如果对宏实参展开后的结果进行字符串化,需使用两层宏。
#define TWO 2 #define STR1(x) STR2(x) #define STR2(x) # x printf("%s\n", STR1(TWO)); //输出2。
字符串化实参成为对应实参的预处理标记序列时其中占位标记已删除;字符串化实参的预处理标记间出现的空白(white space)在字符串字面量中转换成单个空格符(space character);构成字符串化实参的第一个预处理标记之前和最后一个预处理标记之后的空白会被删除。
#define STRINGIZE(x) # x //其中一个□表示一个空格符。 //字符串化的结果是"A□B"。 STRINGIZE(□□A□□□B□□);
一般情况下字符串化实参中每个预处理标记的原始拼写将保留在字符串字面量中,但对字符串字面量和字符常量生成的拼写时会进行特殊处理:在字符常量或者字符串字面量中的每个“字符和\字符前插入\字符(包括分隔的”字符),但对以\字符开头的通用字符名是否在\字符前插入\字符将由实现定义。
#define STRINGIZE(x) # x STRINGIZE(China); //实参字符串化后的预处理标记序列为"China"。 STRINGIZE("Hi"); //实参字符串化后的预处理标记序列为"\"Hi\""。 STRINGIZE(\n); //实参字符串化后的预处理标记序列为"\\n"。
如果替换后的结果不是有效的字符串字面量,其行为是未定义的。
#define STRINGIZE(x) # x STRINGIZE(Hello"); //未定义行为。
空实参字符串化对应的字符串字面量是””。
#define STRINGIZE(x) # x printf("%zu\n", sizeof(STRINGIZE())); //输出1。
#运算符和##运算符的评估顺序是未指定的。
#define MACRO(x, y, z) x # y ## z //如果先执行#,将使用a "b"c替换宏。 //如果先执行##,将使用a "bc"替换宏。 MACRO(a, b, c);
因此应避免在同一个宏中既使用#运算符,又使用##运算符。
4、##运算符
宏展开时有时需要将两个标记合并成一个标记,这称为标记连接(token concatenation)或者标记粘贴(token pasting)。##运算符可以执行标记连接操作。
无论是对象式宏还是函数式宏,在重新检查替换列表寻找更多待替换的宏名前,会删除替换列表中所有非实参来源的##预处理标记,并将##前一个预处理标记与##后一个预处理标记进行连接。
#define TWOHASH # ## # #define JOIN(x, y) x ## y #define STR1(x) STR2(x) #define STR2(x) # x printf("%s\n", STR1(TWOHASH)); //输出##。 printf("%s\n", STR1(JOIN(a, b))); //输出ab。
如果函数式宏的替换列表中某个形参的前面或者后面紧挨着##预处理标记,那么该形参将被相应实参的预处理标记序列替换;如果实参不包含任何预处理标记(即实参为空。),形参会被占位标记(placemarker)替换。占位标记的处理机制:两个占位标记连接生成单个占位标记;占位标记与非占位标记连接生成非占位标记。
(注:占位标记不会出现在语法中,因为占位标记是仅存在于编译阶段步骤4的临时实体。占位标记在字符串化之前移除,并且可以通过重新扫描和进一步替换移除。)
#define JOIN(x, y) x ## y JOIN(a, b); //连接生成ab。 JOIN(a, ); //连接生成a。 JOIN( , b); //连接生成b。 JOIN( , ); //连接生成占位标记。
作为##运算符的操作数,实参不会先进行宏展开。
#define EMPTY #define JOIN(x, y) x ## y JOIN(a, EMPTY); //连接生成aEMPTY。 JOIN(EMPTY, b); //连接生成EMPTYb。
如果对宏实参展开后的结果进行连接,需使用两层宏。
#define EMPTY #define JOIN2(x, y) x ## y #define JOIN1(x, y) JOIN2(x, y) JOIN1(a, EMPTY); //连接生成a。 JOIN1(EMPTY, b); //连接生成b。
无论是函数式宏,还是对象式宏,##预处理标记既不能出现在替换列表的开始处,也不能出现在替换列表的末尾处。
#define MACRO1 ## abc //非法。 #define MACRO2 abc ## //非法。
宏定义时##运算符前后的空格是可选的;但对于对象式宏定义,左操作数最后一个预处理标记和右操作数第一个预处理标记是#的情况除外。
#define JOIN(x, y) x ## y //等价于#define JOIN(x, y) x##y。 #define A # ## # //连接生成##。 #define B #### //非法,##预处理标记不能出现在替换列表的开始或者末尾。 #define C a# ## #b //连接生成a##b。 #define D a####b //连接生成ab。
(注:使用GCC编译器编译,版本号:GCC 13.4.0。)
如果##运算符的结果不是有效的预处理标记,其行为是未定义的。
#define MACRO "Hello " ## World!"
##运算符生成的结果标记可以进一步宏替换。
#define FOUR 4 #define MACRO F ## OUR int x = MACRO; //x的值为4。
##运算符的评估顺序是未指定的。当宏定义中包含多个##运算符时,预处理器可以按任意顺序执行连接操作。
//如何从左向右执行,先连接x和y,再将结果和z连接。 //如何从右向左执行,先连接z和y,再将结果和x连接。 #define JOIN(x, y, z) x ## y ## z JOIN(3, E, 2);
对于GCC编译器,上述连接从左向右执行。
宏定义中包含多个##运算符时,ISO标准未规定评估顺序,因此宏定义时应避免使用多个##运算符;如果必须使用多个##运算符,代码应不依赖特定评估顺序。