文章目錄
C(和C++)中的宏(Macro)屬於編譯器預先處理的範疇,屬於編譯期概念(而非運行期概念)。下面對常遇到的宏的使用問題做了簡單總結。
- C中的預定義宏
- 宏使用中的常見的基礎問題
-
- #符號和##符號的使用
- ...符號的使用
- 宏的解釋方法
- 我們能碰到的宏的使用
- 宏使用中的陷阱
C中的預定義宏
__DATE__ 進行預先處理的日期(“Mmm dd yyyy”形式的字串文字,如May 27 2006)
__FILE__ 代表當前原始碼檔案名稱的字串文字 ,包含了詳細路徑,如G:/program/study/c+/test1.c
__LINE__ 代表當前原始碼中的行號的整數常量
__DATE__ 源檔案編譯日期
__TIME__ 源檔案編譯時間,格式微“hh:mm:ss”,如:09:11:10;
__func__ 當前所在函數名,在編譯器的較高版本中支援
__FUNCTION__ 當前所在函數名
__STDC__ 常數1, 指示是標準相容
__STDC_HOSTED__ 如果是hosted實現,為1; 如果是..實現,為0
__STD_VERSION__ C99為199901L
對於__FILE__,__LINE__,__func__,__FUNCTION__ 這樣的宏,在偵錯工具時是很有用的,因為你可以很容易的知道程式運行到了哪個檔案的那一行,是哪個函數。
而對於__DATE__,__TIME__則可以擷取編譯時間,如如下代碼通過宏擷取編譯時間,並通過sscanf()從中擷取具體的年月日時分秒資料,可在代碼中做相應使用。My Code中是根據此資料作為版本標識,並依此判斷哪個版本新些及是否需要升級。
char * creationDate = __DATE__ ", " __TIME__;
sscanf(creationDate, "%s %d %d, %d:%d:%d", month, &day, &year, &hour, &min, &sec);
關於#和##
在C語言的宏中,#的功能是將其後面的宏參數進行字串化操作(Stringfication),簡單說就是在對它所引用的宏變數通過替換後在其左右各加上一個雙引號。比如下面代碼中的宏:
#define WARN_IF(EXP) \ do{ if (EXP) \ fprintf(stderr, "Warning: " #EXP "\n"); } \ while(0)
那麼實際使用中會出現下面所示的替換過程:
WARN_IF (divider == 0); 被替換為do { if (divider == 0) fprintf(stderr, "Warning" "divider == 0" "\n");} 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##_##dtypedef 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!\n",); 替換為: fprintf(stderr,"Error!\n",);
這是一個語法錯誤,不能正常編譯。這個問題一般有兩個解決方案。首先,GNU CPP提供的解決方案允許上面的宏調用寫成:
myprintf(templt);
而它將會被通過替換變成:
fprintf(stderr,"Error!\n",);
很明顯,這裡仍然會產生編譯錯誤(非本例的某些情況下不會產生編譯錯誤)。除了這種方式外,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)。