C预处理器之宏展开顺序

C 预处理器

Macros

字符串化 Stringizing

有时候我们希望宏变量成为一个字符串,可以使用运算符 # 来实现字符串化,字符串化不单单是增加 ",必要时还有转义字符 \ 的添加。待字符串化的参数包含空格的话处理方式如下:

  • 前导和末尾的空格被移除
  • 参数中间的连续空格减少到一个
#define WARN_IF(EXP)	\
do {				\
	if (EXP)} {		\
		fprintf(stderr, "Warning: " #EXP "\n");	\
	}				\
} while (0)

// 使用如下
WARN_IF(x == 0);

// C 预处理器会展开得到下面的代码,由于宏参数前有 # 运算符,因此它不被展开,而是被字符串化
// 这在宏参数本身是宏表达式的情况下会更明显地看出差异
->
do {
	if (x == 0) {
		fprintf(stderr, "Warning: " "x == 0" "\n");
	}
} while (0);

// C 编译器则会将连续的字符串常量组合成一个完整的字符串
->
do {
	if (x == 0) {
		fprintf(stderr, "Warning: x == 0\n");
	}
} while (0);

基于上面的特性,如果我们希望支持参数本身是宏的场景下展开参数宏,然后再进行字符串化的话,我们需要定义两层宏。

#define __str(x) #x
#define str(x) __str(x)
#define foo 10

//               foo 遇到 # / ## 首先进行字符串化而不是展开宏
__str(foo) --> #foo --> "foo"
//               foo 没有遇到 # / ##,则进行宏展开
str(foo) --> __str(foo) --> __str(10) --> #10 --> "10"

宏展开顺序1

宏展开顺序大致可以归结为:
第一步:首先用实参代替形参,将实参代入宏文本中
第二步:如果实参也是宏,则展开实参
第三步:最后继续处理宏替换后的宏文本,如果仍包含宏,则继续展开
注意:如果在第二步,实参代入宏文本后,实参之前或之后遇到 ###,实参不再展开

例1:

#define cat(a,b) a ## b
宏调用:cat(cat(1, 2), 3)的展开顺序为:
--> cat(cat(1, 2), 3) --> cat(1, 2) ## 3 --> cat(1, 2)3
首先代入实参cat(1, 2)以及 3 到宏文本 a ## b 中;
cat(1,2) 仍是宏,但后面是##,不展开直接进行字符串拼接,结果为:cat(1, 2)3

例2:

#define cat(a, b) a ## b
#define xcat(x, y) cat(x, y)

宏调用xcat(xcat(1, 2), 3)的展开顺序为:
--> xcat(xcat(1,2), 3) --> cat(xcat(1, 2), 3) --> cat(cat(1, 2), 3) --> cat(1 ## 2, 3) --> 1 ##2 ## 3 --> 123
首先代入实参xcat(1, 2)以及3到宏文本cat(x, y)
发现实参xcat(1, 2)也是一个宏,则继续进行实参的宏展开;
发现实参cat(x, y)也是一个宏,则继续进行实参的宏展开;
在例2中,虽然也生成的cat(cat(1, 2), 3),这里是位于中间过程。例2中首先执行实参的宏展开cat(1, 2), 所以结果和例1不一样。

例2中以下顺序是错的:
xcat(xcat(1,2), 3) --> cat(xcat(1, 2), 3) --> xcat(1, 2) ## 3 --> xcat(1, 2)3
展开后的宏文本中还包含宏,优先进行宏参数xcat(1, 2)的展开,之后才会进行外层宏的处理。

#define cat(a,b) a ## b
#define xcat(x,y) cat(x,y)

int  main()
{

   cat(cat(1,2),3);

   xcat(xcat(1,2),3);

   xcat(cat(1,2),3);

    return 0;
}

预编译展开后得到

int main()
{
    cat(1,2)3;

    123;

    123;
    
    return 0;
}

编译阶段打印宏内容2

对于大型C/C++工程,由于宏定义可能散落在各个头文件和Makefile当中。同时条件编译又是大型工程常用的技巧,所以宏定义的大量存在,给代码阅读增加了难度,如果搞不清楚宏定义的情况就无法掌握程序的执行流程。最简单的办法就是使用printf来打印这些宏,但是如果是带参数的宏就比较麻烦了。而且如果使用printf必须重新编译程序,并执行,当我们阅读的是内核源码或驱动程序,那重新执行的代价是相当可观的。如果在编译阶段可以打印宏的内容就可以省去这些麻烦。以下内容就阐述一种可行的方法。

首先介绍 #pragma message
它能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:
#pragma message(“消息文本”)
当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。
当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。假设我们希望判断自己有没有在源代码的什么地方定义了_X86这个宏可以用下面的方法

#ifdef __cpluscplus
#pragma message(“__cplusplus macro activated!”)
#endif

有时候不仅希望知道宏是否定义了,而且希望知道宏定义的内容。
#可以将宏展开为字符串,下面的示例演示如何在编译阶段打印宏内容。

//两个用于测试的宏
#define PI 3.1415926
#define MAX(a,b) (a)>(b) ? (a) :(b)

//首先定义两个辅助宏
#define PRINT_MACRO_HELPER(x) #x
#define PRINT_MACRO(x) #x"="PRINT_MACRO_HELPER(x)

//编译阶段打印宏内容
#pragma message(PRINT_MACRO(PI))
#pragma message(PRINT_MACRO(PI2))
#pragma message(PRINT_MACRO(MAX(a,b)))
#pragma message(PRINT_MACRO(MAX(x,y)))

结果输出
note: #pragma message: PI=3.1415926
//PRINT_MACRO中的两个x都被替换为PI,PRINT_MACRO_HELPER中的#x替换为宏PI的内容
note: #pragma message: PI2=PI2
//PI2不存在的情况下PRINT_MACRO_HELPER将#x换做PI2
note: #pragma message: MAX(a,b)=(a)>(b) ? (a) :(b)
note: #pragma message: MAX(x,y)=(x)>(y) ? (x) :(y)


  1. https://blog.csdn.net/buye1986/article/details/45100339 ↩︎

  2. https://blog.csdn.net/xshbx/article/details/7981564 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值