總結一下C語言中宏的一些特殊用法和幾個容易踩的坑。由於本文主要參考GCC文檔,某些細節(如宏參數中的空格是否處理之類)在別的編譯器可能有細微差別,請參考相應文檔。
宏基礎宏僅僅是在C預處理階段的一種文本替換工具,編譯完之後對二進位代碼不可見。基本用法如下:
#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 };宏名之後帶括號的宏被認為是宏函數。用法和普通函數一樣,只不過在預處理階段,宏函數會被展開。
優點是沒有普通函數保存寄存器和參數傳遞的開銷,展開後的代碼有利於CPU cache的利用和指令預測,速度快。
缺點是可執行代碼體積大。
#define min(X, Y) ((X) < (Y) ? (X) : (Y))那麼
y = min(1, 2);會被擴展成
y = ((1) < (2) ? (1) : (2));
宏特殊用法在宏體中,如果宏參數前加個#,那麼在宏體擴展的時候,宏參數會被擴展成字符串的形式。如:
#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中,如果斷言失敗,可以將失敗的語句輸出到反饋信息中
在宏體中,如果宏體所在標示符中有##,那麼在宏體擴展的時候,宏參數會被直接替換到標示符中。如:
#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 },
...
};這樣就節省了大量時間,提高效率。
幾個坑由於是純文本替換,C預處理器不對宏體做任何語法檢查,像缺個括號、少個分號什麼的預處理器是不管的。
這裡要格外小心,由此可能引出各種奇葩的問題,一下還很難找到根源。
不僅宏體是純文本替換,宏參數也是純文本替換。有以下一段簡單的宏,實現乘法:
#define MULTIPLY(x, y) x * yMULTIPLY(1, 2)沒問題,會正常展開成1 * 2。
有問題的是這種表達式MULTIPLY(1+2, 3),展開後成了1+2 * 3,顯然優先級錯了。
在宏體中,給引用的參數加個括號就能避免這問題。
#define MULTIPLY(x, y) (x) * (y)MULTIPLY(1+2, 3)就會被展開成(1+2) * (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
有如下宏定義:
#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內修改了全局或靜態變量),程序會產生邏輯錯誤。
所以,儘量不要在宏參數中傳入函數調用。
有如下宏定義:
#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))。
注意,這是極不推薦的寫法,程序可讀性極差。
宏參數中若包含另外的宏,那麼宏參數在被代入到宏體之前會做一次完全的展開,除非宏體中含有#或##。
有如下宏定義:
#define AFTERX(x) X_ ## x
#define XAFTERX(x) AFTERX(x)
#define TABLESIZE 1024
#define BUFSIZE TABLESIZEAFTERX(BUFSIZE)會被展開成X_BUFSIZE。因為宏體中含有##,宏參數直接代入宏體。
XAFTERX(BUFSIZE)會被展開成X_1024。
因為XAFTERX(x)的宏體是AFTERX(x),並沒有#或##,所以BUFSIZE在代入前會被完全展開成1024,然後才代入宏體,變成X_1024。
參考資料:http://gcc.gnu.org/onlinedocs/cpp/Macros.html