C語言巨集的高階應用

weixin_34037977發表於2013-02-02

關於#和##在C語言的巨集中,#的功能是將其後面的巨集引數進行字串化操作(Stringfication),簡單說就是在對它所引用的巨集變數通過替換後在其左右各加上一個雙引號。比如下面程式碼中的巨集:

#define WARN_IF(EXP)           
do{ 
     if (EXP)                 
         fprintf(stderr, "Warning: " #EXP "); 
} while(0)

 

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

WARN_IF (divider == 0);被替換為
do {     
   if (divider == 0) 
      fprintf(stderr, "Warning" "divider == 0" ""); 
} while(0);

 

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

而##被稱為連線符(concatenator),用來將兩個Token連線為一個Token。注意這裡連線的物件是Token就行,而不一定是巨集的變數。比如你要做一個選單項命令名和函式指標組成的結構體的陣列,並且希望在函式名和選單項命令名之間有直觀的、名字上的關係。那麼下面的程式碼就非常實用:

struct command { 
   char * name; 
   void (*function) (void); 
};
#define COMMAND(NAME) { 
    NAME, NAME ## _command 
}


// 然後你就用一些預先定義好的命令來方便的初始化一個command結構的陣列了:

struct command commands[] = { COMMAND(quit), COMMAND(help), ... }

 

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

#define LINK_MULTIPLE(a,b,c,d) a##_##b##_##c##_##d
typedef struct _record_type LINK_MULTIPLE
(name,company,position,salary);
// 這裡這個語句將展開為: // typedef struct _record_type name_company_position_salary

 

關於...的使用

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

#define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__)
// 或者
#define myprintf(templt,args...) fprintf(stderr,templt,args)

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

myprintf(templt,);

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

myprintf("Error!",);
替換為:
fprintf(stderr,"Error!",);

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

myprintf(templt);

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

fprintf(stderr,"Error!",);

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

#define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)

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

myprintf(templt);
被轉化為:
fprintf(stderr,templt);

這樣如果templt合法,將不會產生編譯錯誤。

錯誤的巢狀-Misnesting

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

由操作符優先順序引起的問題-Operator Precedence Problem

由於巨集只是簡單的替換,巨集的引數如果是複合結構,那麼通過替換之後可能由於各個引數之間的操作符優先順序高於單個引數內部各部分之間相互作用的操作符優先順序,如果我們不用括號保護各個巨集引數,可能會產生預想不到的情形。比如:

#define ceil_div(x, y) (x + y - 1) / y

那麼

a = ceil_div( b & c, sizeof(int) );

將被轉化為:

a = ( b & c   + sizeof(int) - 1) / sizeof(int); 
// 由於+/-的優先順序高於&的優先順序,那麼上面式子等同於:
a = ( b & (c + sizeof(int) - 1)) / sizeof(int);

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

#define ceil_div(x, y) (((x) + (y) - 1) / (y))

消除多餘的分號-Semicolon Swallowing

通常情況下,為了使函式模樣的巨集在表面上看起來像一個通常的C語言呼叫一樣,通常情況下我們在巨集的後面加上一個分號,比如下面的帶參巨集:

MY_MACRO(x);

但是如果是下面的情況:

#define MY_MACRO(x) { 
/* line 1 */
/* line 2 */
/* line 3 */
}
//...
if (condition()) MY_MACRO(a); else {...}

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

#define MY_MACRO(x) do { /* line 1 */ /* line 2 */ /* line 3 */ } while(0)

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

Duplication of Side Effects

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

#define min(X,Y) ((X) > (Y) ? (Y) : (X))
//...
c = min(a,foo(b));

這時foo()函式就被呼叫了兩次。為了解決這個潛在的問題,我們應當這樣寫min(X,Y)這個巨集:

#define min(X,Y) ({ typeof (X) x_ = (X); typeof (Y) y_ = (Y); (x_ < y_) ? x_ : y_; })

({...})的作用是將內部的幾條語句中最後一條的值返回,它也允許在內部宣告變數(因為它通過大括號組成了一個區域性Scope)

相關文章