C語言巨集定義的特殊用法以及避坑指南
巨集基礎
巨集僅僅是在C預處理階段的一種文字替換工具,編譯完之後對二進位制程式碼不可見。基本用法如下:
1.標示符別名
#define BUFFER_SIZE 1024
預處理階段
foo = (char *)malloc(BUFFER_SIZE);
會被替換成
foo = (char *)malloc(1024);
巨集體換行需要在行末加反斜槓‘\’
#define NUMBERS 1, \ 2, \ 3
預處理階段
int x[] = {NUMBERS};
會被替換成
int x[] = {1, 2, 3};
2.巨集函式
巨集名之後帶括號的巨集被認為是巨集函式。用法和普通函式一樣,只不過在預處理階段,巨集函式會被展開。
優點:沒有普通函式儲存暫存器和引數傳遞的開銷,展開後的程式碼有利於CPU cache的利用和指令預測,速度快。
缺點:可執行程式碼體積大。
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
那麼
y = min(3, 4);
會被擴充套件成
y = ((3) < (4) ? (3) : (4));
巨集特殊用法
1.字串化(Stringification)
在巨集體中,如果巨集引數前加個#,那麼在巨集體擴充套件的時候,巨集引數會被擴充套件成字串的形式。如:
#define WARN_IF(EXP)\ do{\
if(EXP)\ fprintf(stderr, "Warning: " #EXP "\n");\
}while(0)
呼叫
WARN_IF (x == 0);
會被擴充套件成
do{
if(x == 0) fprintf(stderr, "Warning: " "x == 0" "\n");
}while(0);
這種用法可以用在assert中,如果斷言失敗,可以將失敗的語句輸出到反饋資訊中。
2.連線(Concatenation)
在巨集體中,如果巨集體所在標示符中有##,那麼在巨集體擴充套件的時候,巨集引數會被直接替換到標示符中。如:
#define COMMAND(NAME) {#NAME, NAME ## _command}
那麼
struct command { char *name; void (*function)(void); };
在巨集擴充套件的時候
struct command commands[] = { COMMAND(quit), COMMAND(help), ... };
會被擴充套件成:
struct command commands[] = { {"quit", quit_command}, {"help", help_command}, ... };
避坑
1.語法問題
由於是純文字替換,C前處理器不對巨集體做任何語法檢查,像缺個括號、少個分號等前處理器是不管的。
這裡要格外小心,由此可能引出各種奇葩的問題,一下還很難找到根源。
2.運算子優先順序問題
不僅巨集體是純文字替換,巨集引數也是純文字替換。有以下一段簡單的巨集,實現乘法:
#define MULTIPLY(x, y) x * y
MULTIPLY(1, 2)沒問題,會正常展開成1 * 2。
有問題的是這種表示式MULTIPLY(1+2, 3),展開後成了1+2 * 3,顯然優先順序錯。
其實這個問題和下面要說到的某些問題都屬於由於純文字替換而導致的語義破壞問題,要格外小心。
3.分號吞噬問題
有如下巨集定義:
#define SKIP_SPACES(p, limit)\ {\ char *lim = (limit);\ while(p < lim)\ {\ if(*p++ != ' ')\ {\ p--; break;\ }\ }\ }
假設有如下一段程式碼:
if (*p != 0) SKIP_SPACES(p, lim); else ...
此時巨集替換後代碼為
if (*p != 0) { ...... }; else ...
編譯器發現這個else沒有對應的if。
這個問題一般用do ... while(0)的形式來解決:
#define SKIP_SPACES(p, limit)\ do{\ char *lim = (limit);\ while(p < lim)\ {\ if(*p++ != ' ')\ {\ p--; break;\ }\ }\ }while(0)
4.巨集引數重複呼叫
有如下巨集定義:
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
當有如下呼叫時
next = min(x + y, foo (z));
巨集體被展開成
next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));
可以看到,foo(z)被重複呼叫了兩次,做了重複計算。
更嚴重的是,如果foo是不可重入的(foo內修改了全域性或靜態變數),程式會產生邏輯錯誤。
所以,儘量不要在巨集引數中傳入函式呼叫。
5.對自身的遞迴引用
有如下巨集定義:
#define foo (4 + foo)
按前面的理解,(4 + foo)會展開成(4 + (4 + foo)),然後一直展開下去,直至記憶體耗盡。
但是,前處理器採取的策略是隻展開一次。也就是說,foo只會展開成(4 + foo),而展開之後foo的含義就要根據上下文來確定了。
對於以下的交叉引用,巨集體也只會展開一次。
#define x (4 + y) #define y (2 * x)
注意:這是極不推薦的寫法,程式可讀性極差。
6.巨集引數預處理
巨集引數中若包含另外的巨集,那麼巨集引數在被代入到巨集體之前會做一次完全的展開,除非巨集體中含有#或##。
有如下巨集定義:
#define AFTERX(x) X_ ## x #define XAFTERX(x) AFTERX(x) #define TABLESIZE 1024 #define BUFSIZE TABLESIZE
AFTERX(BUFSIZE)被展開成X_BUFSIZE。因為巨集體中含有##,巨集引數直接代入巨集體。
XAFTERX(BUFSIZE)會被展開成X_1024。
因為XAFTERX(x)的巨集體是AFTERX(x),並沒有#或##,所以BUFSIZE在代入前會被完全展開成1024,然後才代入巨集體,變成X_1024。