【轉】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

-EOF-

 

參考資料:

http://gcc.gnu.org/onlinedocs/cpp/Macros.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM