关于C里面宏替换的问题

先看一个经典的面试题:

#include <stdio.h>
#define f(a,b) a##b
#define g(a) #a
#define h(a) g(a)

int main()
{
printf("%s\n", h(f(1,2)));
printf("%s\n", g(f(1,2)));
return 0;
}

输出是:

12

f(1,2)

原因就是宏替换的原则问题:

当一个宏参数被放进宏体时,通常(注意,有例外)这个宏参数会首先被全部展开。当展开后的宏参数被放进宏体时,
预处理器对新展开的宏体进行第二次扫描,并继续展开。例如:
#define PARAM( x ) x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) );
因为ADDPARAM( 1 ) 是作为PARAM的宏参数,所以先将ADDPARAM( 1 )展开为INT_1,然后再将INT_1放进PARAM。

例外情况是,如果PARAM宏里对宏参数使用了#或##,那么宏参数不会被展开:
#define PARAM( x ) #x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) ); 将被展开为"ADDPARAM( 1 )"。

现在问题明朗了,为什么两次的输出结果不一样,就是因为#define g(a) #a这个宏,它不会对传入的行参做替换,因此第二行的输出为f(1,2).

下面是一个更详细的讨论贴,究其缘由还是从C的标准引起的,看来还是得把游戏规则弄清楚才能玩得溜啊,继续修炼

==================================================================================

上午突然看到CSDN上某一贴里对这个宏递归替换机制的火热的讨论,到最后居然还是未定义的结果,CSDN现在真是越来越不行了。有兴趣看了看,晚上过来做了点实践和思考,研究了一下有了结论: 


在宏定义中,#表示将其后的内容转换为字符串,##表示将它前后两个TOKEN连接为一个,另外宏也可以带参数,看起来就像个函数,只是实参是用来替换宏体内相应形参的,例如:

#define aaa #aaa

#define cat(a,b) a ## b

void main()

{

cout<<aaa<<endl;

cout<<cat(1,2)<<endl;

}

这个程序会输出两个字符串:

aaa

12

讨论帖地址:http://topic.csdn.net/u/20090727/18/457c61bd-7461-431c-bbf9-924865cfe43c.html
这个楼主给了这样一个递归宏(就是带参数的宏它的参数里面还有宏)
#define cat(a,b) a ## b
#define f(a) fff a
#define ab AB
cat(cat(1,2),3)
cat(a,b)
f(cat(cat(1,2),3))

lz的答案是:

cat(1,2)3

AB

fff 123

lz给的答案不知道使用的哪个编译器的结果,在微软的编译器下结果是:
cat(1,2)3
AB
fff cat(1,2)3
而且不管什么编译器,lz给的结果明显第一个和第三个不匹配,理应处理方法是一样的。所以lz的结果我是很怀疑是错的,至少在微软的编译器第一个是对的,第三个是错的。
既然提出结合标准来看,首先引用出与这部分有关的标准吧:

The sequence of preprocessing tokens bounded by the outside-most matching parentheses
forms the list of arguments for the function-like macro. The individual arguments within the list are separated by comma preprocessing tokens. but comma preprocessing tokens between matching inner parentheses do not separate arguments.

6.8.3.1 Argument substitution
After the arguments for the invocation of a function-like niacro have been identified.argument substitution takes place. A parameter in the replacement list. unless preceded by a # or ## preprocessing token or followed by a ## preprocessing token (see below). is replaced by the corresponding argument after all macros contained therein have been expanded. Before being substituted, each argument’s preprocessing tokens are completely macro replaced as if they formed the rest of the translation unit: no other preprocessing tokens are available.



It is often useful to merge two tokens into one while expanding macros. This is called token pasting or token concatenation. The `##' preprocessing operator performs token pasting. When a macro is expanded, the two tokens on either side of each `##' operator are combined into a single token, which then replaces the `##' and the two original tokens in the macro expansion. Usually both will be identifiers, or one will be an identifier and the other a preprocessing number. When pasted, they make a longer identifier. This isn't the only valid case. It is also possible to concatenate two numbers (or a number and a name, such as 1.5 and e3) into a number. Also, multi-character operators such as += can be formed by token pasting. 


However, two tokens that don't together form a valid token cannot be pasted together. For example, you cannot concatenate x with + in either order. If you try, the preprocessor issues a warning and emits the two tokens. Whether it puts white space between the tokens is undefined. It is common to find unnecessary uses of `##' in complex macros. If you get this warning, it is likely that you can simply remove the `##'. 

总结上述的内容,就是两点:[引用supermegaboy的回复]
1.最外层括号中的逗号为实参分隔符,内层的不分隔该层实参;
2.带有预处理指令#或##的形参,不参与宏替换或宏展开

对于:
#define cat(a,b) a ## b
cat(cat(1,2),3)
supermegaboy分析得很正确,见6楼。

而对于:
#define cat(a,b) a##b
#define xcat(a,b) cat(a,b)
xcat(xcat(1,2),3)

supermegaboy用简化的步骤来一层一层替换下去:
xcat(xcat(1,2),3)
xcat(cat(1,2),3)
xcat(1##2, 3)
cat(1##2,3)
1##2##3
123
答案是对的,至少微软编译器得到也是这个,可是同样的编译器对于f(cat(cat(1,2),3))的解释却和用supermegaboy的方法得到的结果不一致,说明supermegaboy的方法不对。

呵呵,这个问题其实应该这么看:
对于:
#define cat(a,b) a ## b
cat(cat(1,2),3)
很多回复的朋友都有各种各样的结论,有的说是从最外层往里开始替换的,也有的说是从最里层往外替换的,两种说法都能刚好解释一些特例而已,其实两者都不完全对,通过我的实践和对标准的理解得出微软的编译器的处理办法如下:
cat(cat(1,2),3)
cat(1,2) ## 3
cat(1,2)3 //由规则2得到cat(1,2)和3即使是宏也不进行求值

而对于:
#define cat(a,b) a##b
#define xcat(a,b) cat(a,b)
xcat(xcat(1,2),3)
事实上微软和大部分编译器都是这样进行替换的:
xcat(xcat(1,2),3)
cat(xcat(1,2),3) //由规则2得到xcat(1,2)是一个宏且没有相关的#和##,因此对此宏进行求值展开,注意是完全展开,而不是只展开一层,同时当前宏展开的编译器程序暂停,递归调用了另一个宏替换程序
xcat(1,2) -> cat(1,2) -> 1 ## 2 ->12 //该宏替换子程序结束返回结果12给上一级宏替换程序
cat(12,3) //主宏替换程序继续进行,xcat(1,2)没有相连的#,##所以求值展开为12
123 //最后结果就很明显了

现在再来看lz这个问题:
cat(cat(1,2),3)
cat(a,b)
f(cat(cat(1,2),3))
第一个已经解决,第二个展开如下:
cat(a,b)
a ## b //由标准规则2知道,即使a,b是宏在这里也不进行求值展开了
ab //主宏替换程序结束,再次扫描发现ab还是一个宏,且没有相连的#,##因此调用宏展开子程序求其值
ab -> AB
AB //再次扫描已经没有宏了,主宏替换程序结束

第三个宏:
f(cat(cat(1,2),3)) //经扫描发现有递归宏,从最外层开始替换,遇到需要展开的宏则调用宏展开子程序
fff cat(cat(1,2),3) //由规则2继续,对cat(cat(1,2),3)调用宏展开子程序
cat(cat(1,2),3) -> cat(1,2) ## 3 ->cat(1,2)3 //由于cat(1,2)3不是宏,宏替换子程序结束并返回
fff cat(1,2)3

再对supermegaboy的另一个问题解释一下:
#define N 10
如果想制造字符串化效果"10",直接#define X(N) #N是不行的,根据第一点,结果只会是"N",而不是"10",这时候应该这样定义宏:
#define N 10
#define Y(N) X(N)
#define X(N) #N
对于他的这部分说明,我们来用我刚刚的方法进行展开即可:
首先:
#define N 10
#define X(N) #N

宏替换开始:X(N) -> #N -> "N",因为N遇到了#,由标准可知不对它展开
为什么

#define N 10
#define Y(N) X(N)
#define X(N) #N
这样行呢?
依次展开就明白了:Y(N) -> X(N) -> X(10) -> #10 ->"10" //在第二步时N没有#,##因此在对X(N)递归展开时先对N展开了,N展开后再继续对X(N)的展开。

我写两个宏,有需要看各种宏展开的可以像这样用:
#define TOSTR_HELPER(a) #a
#define TOSTR(a) TOSTR_HELPER(a)
#define ... //定义你想要看最后展开结果的递归宏

void main()
{
std::cout<<TOSTR(...)<<endl;
}
这样就可以看TOSTR()中宏的展开情况了,因为它会把里面的宏展开结果转化成字符串并输出。

思考:编译器其实也是一个程序,单纯从最外层向最里层替换的操作在程序上不好设计,单纯从最里向最外层替换也一样,所以实际的替换过程其实是里外都在进行,我用伪代码写一个可以实现上述方法的替换程序,猜测那些编译器大致是这么处理的:

TEXT MacroSubstitute(TEXT Macro , TEXT macro[])

{

    扫描该Macro分离出该Macro的参数TEXT parameter[...](如果有的话);

    if(该Macro不被#和##修饰)

        Macro=为其定义的宏;            //参数还没有展开,只针对宏体

    else

        return Macro;                //如果被修饰则不对它展开直接返回

    for(对该Macro的参数进行遍历 : i=0 -> N)

        if(parameter[i]存在于macro[]中)

            parameter[i]=MacroSubstitute(parameter[i],macro); //对参数进行展开,递归调用宏替换程序

    if(Macro在macro[]中)                //被展开的宏体仍然是宏

        Macro(...)=Macro(parameter[0],parameter[1]...);//用已经展开的参数替换原来的参数形成新的宏

    return MacroSubstitute(Macro,macro);        //最后把这个新宏再按照宏替换的方式展开返回

}

最后总结一下吧:

标准没有规定替换的具体实现,只是提到了一些需要注意的规则,所以替换的具体实现在不同的编译器下是不同的,目前为止,GNU和微软的编译器处理办法就是我上面所说的那样,算是标准的一种实现吧。

问题是解决了,但是建议这样的宏尽量不要用,现在已经迈入标准C++的时代了还是用const和inline吧
发布了17 篇原创文章 · 获赞 6 · 访问量 1万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览