關於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吧
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章