總結一下C語言中宏的一些特殊用法和幾個容易踩的坑。因爲本文主要參考GCC文檔,某些細節(如宏參數中的空格是否處理之類)在別的編譯器可能有細微差異,請參考相應文檔。html
宏基礎
宏僅僅是在C預處理階段的一種文本替換工具,編譯完以後對二進制代碼不可見。基本用法以下:函數
1. 標示符別名
#define BUFFER_SIZE 1024
預處理階段,foo = (char *) malloc (BUFFER_SIZE);
會被替換成foo = (char *) malloc (1024);
工具
宏體換行須要在行末加反斜槓\ui
#define NUMBERS 1, \ 2, \ 3
預處理階段int x[] = { NUMBERS };
會被擴展成int x[] = { 1, 2, 3 };
spa
2. 宏函數
宏名以後帶括號的宏被認爲是宏函數。用法和普通函數同樣,只不過在預處理階段,宏函數會被展開。優勢是沒有普通函數保存寄存器和參數傳遞的開銷,展開後的代碼有利於CPU cache的利用和指令預測,速度快。缺點是可執行代碼體積大。code
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
y = min(1, 2);
會被擴展成y = ((1) < (2) ? (1) : (2));
htm
宏特殊用法
1. 字符串化(Stringification)
在宏體中,若是宏參數前加個#
,那麼在宏體擴展的時候,宏參數會被擴展成字符串的形式。如:遞歸
#define WARN_IF(EXP) \ do { if (EXP) \ fprintf (stderr, "Warning: " #EXP "\n"); } \ while (0)
WARN_IF (x == 0);
會被擴展成:ip
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
。
-EOF-
參考資料: