C語言巨集的特殊用法和幾個坑 (轉)
總結一下C語言中巨集的一些特殊用法和幾個容易踩的坑。由於本文主要參考GCC文件,某些細節(如巨集引數中的空格是否處理之類)在別的編譯器可能有細微差別,請參考相應文件。
巨集基礎
巨集僅僅是在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(1, 2);
會被擴充套件成y = ((1) < (2) ? (1) : (2));
巨集特殊用法
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
,顯然優先順序錯了。
在巨集體中,給引用的引數加個括號就能避免這問題。
#define MULTIPLY(x, y) (x) * (y)
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 ...
一編譯,GCC報error: ‘else’ without a previous ‘if’
。原來這個看似是一個函式的巨集被展開後是一段大括號括起來的程式碼塊,加上分號之後這個if邏輯塊就結束了,所以編譯器發現這個else沒有對應的if。
這個問題一般用do ... while(0)
的形式來解決:
#define SKIP_SPACES(p, limit) \ do { char *lim = (limit); \ while (p < lim) { \ if (*p++ != ' ') { \ p--; break; }}} \ while (0)
展開後就成了
if (*p != 0) do ... while(0); else ...
這樣就消除了分號吞噬問題。
這個技巧在Linux核心原始碼裡很常見,比如這個置位巨集#define SET_REG_BIT(reg, bit) do { (reg |= (1 << (bit))); } while (0)
(位於arch/mips/include/asm/mach-pnx833x/gpio.h)
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)
x
展開成(4 + y) -> (4 + (2 * x))
,y
展開成(2 * x) -> (2 * (4 + y))
。
注意,這是極不推薦的寫法,程式可讀性極差。
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
。<