天天看點

C語言宏定義##連接配接符和#符的使用

C語言中如何使用宏C(和C++)中的宏(Macro)屬于編譯器預處理的範疇,屬于編譯期概念(而非運作期概念)。下面對常遇到的宏的使用問題做了簡單總結。

在C語言的宏中,#的功能是将其後面的宏參數進行字元串化操作(Stringfication),簡單說就是在對它所引用的宏變量通過替換後在其左右各加上一個雙引号。比如下面代碼中的宏:

那麼實際使用中會出現下面所示的替換過程:

這樣每次divider(除數)為0的時候便會在标準錯誤流上輸出一個提示資訊。

而##被稱為連接配接符(concatenator),用來将兩個Token連接配接為一個Token。注意這裡連接配接的對象是Token就行,而不一定是宏的變量。比如你要做一個菜單項指令名和函數指針組成的結構體的數組,并且希望在函數名和菜單項指令名之間有直覺的、名字上的關系。那麼下面的代碼就非常實用:

COMMAND宏在這裡充當一個代碼生成器的作用,這樣可以在一定程度上減少代碼密度,間接地也可以減少不留心所造成的錯誤。我們還可以n個##符号連接配接 n+1個Token,這個特性也是#符号所不具備的。比如:

...在C宏中稱為Variadic Macro,也就是變參宏。比如:

第一個宏中由于沒有對變參起名,我們用預設的宏__VA_ARGS__來替代它。第二個宏中,我們顯式地命名變參為args,那麼我們在宏定義中就可以用args來代指變參了。同C語言的stdcall一樣,變參必須作為參數表的最有一項出現。當上面的宏中我們隻能提供第一個參數templt時,C标準要求我們必須寫成:

的形式。這時的替換過程為:

這是一個文法錯誤,不能正常編譯。這個問題一般有兩個解決方法。首先,GNU CPP提供的解決方法允許上面的宏調用寫成:

而它将會被通過替換變成:

很明顯,這裡仍然會産生編譯錯誤(非本例的某些情況下不會産生編譯錯誤)。除了這種方式外,c99和GNU CPP都支援下面的宏定義方式:

這時,##這個連接配接符号充當的作用就是當__VAR_ARGS__為空的時候,消除前面的那個逗号。那麼此時的翻譯過程如下:

這樣如果templt合法,将不會産生編譯錯誤。 這裡列出了一些宏使用中容易出錯的地方,以及合适的使用方式。

宏的定義不一定要有完整的、配對的括号,但是為了避免出錯并且提高可讀性,最好避免這樣使用。

由于宏隻是簡單的替換,宏的參數如果是複合結構,那麼通過替換之後可能由于各個參數之間的操作符優先級高于單個參數内部各部分之間互相作用的操作符優先級,如果我們不用括号保護各個宏參數,可能會産生預想不到的情形。比如:

那麼

将被轉化為:

這顯然不是調用者的初衷。為了避免這種情況發生,應當多寫幾個括号:

通常情況下,為了使函數模樣的宏在表面上看起來像一個通常的C語言調用一樣,通常情況下我們在宏的後面加上一個分号,比如下面的帶參宏:

但是如果是下面的情況:

這樣會由于多出的那個分号産生編譯錯誤。為了避免這種情況出現同時保持MY_MACRO(x);的這種寫法,我們需要把宏定義為這種形式:

這樣隻要保證總是使用分号,就不會有任何問題。

這裡的Side Effect是指宏在展開的時候對其參數可能進行多次Evaluation(也就是取值),但是如果這個宏參數是一個函數,那麼就有可能被調用多次進而達到不一緻的結果,甚至會發生更嚴重的錯誤。比如:

這時foo()函數就被調用了兩次。為了解決這個潛在的問題,我們應當這樣寫min(X,Y)這個宏:

({...})的作用是将内部的幾條語句中最後一條的值傳回,它也允許在内部聲明變量(因為它通過大括号組成了一個局部Scope)。

繼續閱讀