C語言巨集的特殊用法和幾個坑

2021-07-08 13:18:38 字數 3667 閱讀 8449

2 years ago   |  

source

總結一下c語言中巨集的一些特殊用法和幾個容易踩的坑。由於本文主要參考gcc文件,某些細節(如巨集引數中的空格是否處理之類)在別的編譯器可能有細微差別,請參考相應文件。

巨集僅僅是在c預處理階段的一種文字替換工具,編譯完之後對二進位制**不可見。基本用法如下:

1. 標示符別名

#define buffer_size 1024
預處理階段,foo = (char *) malloc (buffer_size);會被替換成foo = (char *) malloc (1024);

巨集體換行需要在行末加反斜槓\

#define numbers 1, \

2, \

3

預處理階段int x = ;會被擴充套件成int x = ;

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 \

while (0)

warn_if (x == 0);會被擴充套件成:

do

while(0

);

這種用法可以用在assert中,如果斷言失敗,可以將失敗的語句輸出到反饋資訊中

2. 連線(concatenation)

在巨集體中,如果巨集體所在標示符中有##,那麼在巨集體擴充套件的時候,巨集引數會被直接替換到標示符中。如:

#define command(name)  

struct

command

;

在巨集擴充套件的時候

struct

command

commands

=;

會被擴充套件成:

struct

command

commands

=,,...

};

這樣就節省了大量時間,提高效率。

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)  \

}}

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 }} \

while (0)

展開後就成了

if(*

p!=0)

do...

while(0

);else

...

這樣就消除了分號吞噬問題。

這個技巧在linux核心原始碼裡很常見,比如這個置位巨集#define set_reg_bit(reg, bit) do 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

-eof-

C語言巨集定義的幾個坑和特殊用法

巨集僅僅是在c預處理階段的一種文字替換工具,編譯完之後對二進位制 不可見。基本用法如下 標示符別名 define buffer size 1024預處理階段,foo char malloc buffer size 會被替換成foo char malloc 1024 巨集體換行需要在行末加反斜槓 de...

巨集的特殊用法

的作用就是將 後邊的巨集引數進行字串的操作,也就是將 後邊的引數兩邊加上一對雙引號使其成為字串。例如a是乙個巨集的形參,則替換文字中的 a被系統轉化為 a 這個轉換過程即為字串化。define test param param char pstr test 123 printf psrt s n p...

C語言巨集中的 和 的用法

與 在巨集定義中的 巨集展開 include define f a,b a b define g a a define h a g a int main 巨集展開時 如果巨集定義以 開頭,不展開引數,直接替換。故g f 1,2 f 1,2 f 1,2 如果巨集定義不以 開頭,展開引數,直接替換,由外...