深入理解C語言中宏定義#, ##

從本質上看,C語言中的宏定義實現的是一個文本替換的功能,似乎很簡單的樣子,然而這幾天去看了下Linux Kernel源碼中的各種宏定義,才發現一個宏定義竟然也可以有如此多的奇技淫巧……於是花了一天時間仔細研究了下宏的相關知識,此處整理總結下。

關於宏,網上有一組寫得極好的文章,基本上看完這幾篇文章就可以對宏有一個深入的理解了:

宏定義黑魔法-從入門到奇技淫巧 (1) —— 基本概念
宏定義黑魔法-從入門到奇技淫巧 (2) —— object-like宏的展開
宏定義黑魔法-從入門到奇技淫巧 (3) —— function-like宏的展開
宏定義黑魔法-從入門到奇技淫巧 (4) —— 一些宏的高級用法
宏定義黑魔法-從入門到奇技淫巧 (5) —— 圖靈完備
宏定義黑魔法-從入門到奇技淫巧 (6) —— 宏的一些坑

作者的知乎上也有一份相同的備份。

相同的內容此處就不再重複了,此處列出一些要點。

帶參數的宏中可以使用兩個特殊運算符,#(Stringification Operator)和##(Token Pasting Operator),作用分別是把宏參數變爲字符串字面量和連接兩個token。且遇到這兩個運算符時,宏參數不會展開。
宏的嵌套展開過程中,已展開過的宏不會重複展開。
宏展開後,會進一步檢查是否構成了新的宏,若構成了會進一步展開。
宏定義中也可以使用…代表可變參數,用__VA_ARGS__獲取可變參數列表。
宏參數會先展開,之後再進行替換,這也被稱爲”prescan”.
宏基本上是圖靈完備的,所以可以只靠宏實現各種東西……
宏的展開過程遵循以下流程圖:

這個流程圖是我根據自己的理解和實驗畫出來的,並不確定完全正確……圖中的“已展開宏記錄”就是文章中說的“藍色列表”。

使用gcc編譯時,可以通過附加-E參數,讓gcc只進行預處理,這樣就可以看到各種宏實際展開出來的結果是什麼了,如gcc -E -o test.i test.c命令對test.c文件進行預處理,生成test.i文件。

關於宏的展開流程,有一些不太明確的地方,此處用例子說明下,結果都經過gcc預處理驗證過。

#define P 2
#define M K(P)
#define K(a) a a ## a a
#define PP 11
#define FOO(a) #a a

FOO(M)	-->  FOO(K(P))
        -->  FOO(2 PP 2)
        -->  FOO(2 11 2)
        -->  "M" 2 11 2

遇到#或##時,其相連的宏參數不會展開,然而這不意味着這個宏參數本身不會展開,其他部分用到這個宏參數的地方還是會展開的。

另外,#運算符後必須是一個宏參數,不能是其他東西,不過##兩端則無這一要求,來看個例子:

#define P(a)  #a a
#define PP(a) #a a
#define CAT(a) P ## P(P##a(a))

CAT(P)	-->  PP(PP(P))
        -->  PP("P" P)
        -->  "PP(P)" "P" P

CAT(A)	-->  PP(PA(A))
        -->  "PA(A)" PA(A)

可以看到,##兩端可以是任意token,其作用就是把這兩個token合成一個。另外,還可以看到,##是在最開始就進行處理的,所以P(a)這個宏是沒有用到的。另外,由##操作符組合產生的新宏是會繼續展開的,並不像某些文章說的那樣會停止展開。

關於多次掃描展開的問題,有些文章中說的是展開完成後會重新掃描一遍當前字符串,若有可以繼續展開的則繼續展開,然而實際測試下來並不是這樣的。還是來看個例子:

#define BRACKET ()
#define CREATE(a, b)  a ## b
#define FOO() 123

CREATE(F, OO) BRACKET   -->  FOO BRACKET
                        -->  FOO ()

CREATE(F, OO) ()        -->  FOO ()
                        -->  123

FOO BRACKET             -->  FOO ()

可以看到,第1、3個例子中,展開到FOO () 之後就沒有繼續展開下去了,這說明並沒有重新掃描字符串這一步,已經處理過的部分不會再次處理的。而第2個例子則說明的確是會再展開合併出新的宏來的,故上面的流程圖中使用了”向後掃描一個token,形成一個新的字符串”這樣的說法。考慮到token是以空白爲界劃分的,後面組合出來的新宏只可能是function-like的宏,所以這樣的展開方式是不存在歧義的,不會出現原來的宏被組合成其他宏的情況。

如果要繼續展開上面未能展開的那兩個宏,可以再封裝一層:

#define BRACKET ()
#define CREATE(a, b)  a ## b
#define FOO() 123
#define EXPAND(...) __VA_ARGS__

EXPAND(CREATE(F, OO) BRACKET)	-->  EXPAND(FOO())
                                -->  EXPAND(123)
                                -->  123

這裏利用的原理是:宏參數會先儘可能展開後再進行替換。

將宏定義的各種奇技淫巧應用得巔峯造極、神鬼莫測之作就是The Boost Preprocessing library,這是Boost庫的一部分,不過和其他部分完全獨立,這部分包含了各種數據結構和算法等,而且只有頭文件,全部都是宏定義……簡直可謂是喪心病狂……Github上有人將這部分獨立的代碼提取出來了,有興趣的讀者可以去進一步揣摩瞻仰:boost-preprocessor

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章