較大的項目都會用大量的宏定義來組織代碼,你可以看看/usr/include
下面的標頭檔中用了多少個宏定義。看起來宏展開就是做個替換而已,其實裡面有比較複雜的規則,C語言有很多複雜但不常用的語法規則本書並不涉及,但有關宏展開的語法規則本節卻力圖做全面講解,因為它很重要也很常用。
以前我們用過的#define N 20
或#define STR "hello, world"
這種宏定義可以稱為變數式宏定義(Object-like Macro),宏定義名可以像變數一樣在代碼中使用。另外一種宏定義可以像函數調用一樣在代碼中使用,稱為函數式宏定義(Function-like Macro)。例如編輯一個檔案main.c
:
#define MAX(a, b) ((a)>(b)?(a):(b)) k = MAX(i&0x0f, j&0x0f)
我們想看第二行的表達式展開成什麼樣,可以用gcc
的-E
選項或cpp
命令,儘管這個C程序不合語法,但沒關係,我們只做預處理而不編譯,不會檢查程序是否符合C語法。
$ cpp main.c # 1 "main.c" # 1 "<built-in>" # 1 "<command-line>" # 1 "main.c" k = ((i&0x0f)>(j&0x0f)?(i&0x0f):(j&0x0f))
就像函數調用一樣,把兩個實參分別替換到宏定義中形參a
和b
的位置。注意這種函數式宏定義和真正的函數調用有什麼不同:
1、函數式宏定義的參數沒有類型,預處理器只負責做形式上的替換,而不做參數類型檢查,所以傳參時要格外小心。
2、調用真正函數的代碼和調用函數式宏定義的代碼編譯生成的指令不同。如果MAX
是個真正的函數,那麼它的函數體return a > b ? a : b;
要編譯生成指令,代碼中出現的每次調用也要編譯生成傳參指令和call
指令。而如果MAX
是個函數式宏定義,這個宏定義本身倒不必編譯生成指令,但是代碼中出現的每次調用編譯生成的指令都相當於一個函數體,而不是簡單的幾條傳參指令和call
指令。所以,使用函數式宏定義編譯生成的目標檔案會比較大。
3、定義這種宏要格外小心,如果上面的定義寫成#define MAX(a, b) (a>b?a:b)
,省去內層括號,則宏展開就成了k = (i&0x0f>j&0x0f?i&0x0f:j&0x0f)
,運算的優先順序就錯了。同樣道理,這個宏定義的外層括號也是不能省的,想一想為什麼。
4、調用函數時先求實參表達式的值再傳給形參,如果實參表達式有Side Effect,那麼這些Side Effect只發生一次。例如MAX(++a, ++b)
,如果MAX
是個真正的函數,a
和b
只增加一次。但如果MAX
是上面那樣的宏定義,則要展開成k = ((++a)>(++b)?(++a):(++b))
,a
和b
就不一定是增加一次還是兩次了。
5、即使實參沒有Side Effect,使用函數式宏定義也往往會導致較低的代碼執行效率。下面舉一個極端的例子,也是個很有意思的例子。
例 21.1. 函數式宏定義
#define MAX(a, b) ((a)>(b)?(a):(b)) int a[] = { 9, 3, 5, 2, 1, 0, 8, 7, 6, 4 }; int max(int n) { return n == 0 ? a[0] : MAX(a[n], max(n-1)); } int main(void) { max(9); return 0; }
這段代碼從一個數組中找出最大的數,如果MAX
是個真正的函數,這個算法就是從前到後遍歷一遍數組,時間複雜度是Θ(n),而現在MAX
是這樣一個函數式宏定義,思考一下這個算法的時間複雜度是多少?
儘管函數式宏定義和真正的函數相比有很多缺點,但只要小心使用還是會顯著提高代碼的執行效率,畢竟省去了分配和釋放棧幀、傳參、傳返回值等一系列工作,因此那些簡短並且被頻繁調用的函數經常用函數式宏定義來代替實現。例如C標準庫的很多函數都提供兩種實現,一種是真正的函數實現,一種是宏定義實現,這一點以後還要詳細解釋。
函數式宏定義經常寫成這樣的形式(取自內核代碼include/linux/pm.h
):
#define device_init_wakeup(dev,val) \ do { \ device_can_wakeup(dev) = !!(val); \ device_set_wakeup_enable(dev,val); \ } while(0)
為什麼要用do { ... } while(0)
括起來呢?不括起來會有什麼問題呢?
#define device_init_wakeup(dev,val) \ device_can_wakeup(dev) = !!(val); \ device_set_wakeup_enable(dev,val); if (n > 0) device_init_wakeup(d, v);
這樣宏展開之後,函數體的第二條語句不在if
條件中。那麼簡單地用{ ... }
括起來組成一個語句塊不行嗎?
#define device_init_wakeup(dev,val) \ { device_can_wakeup(dev) = !!(val); \ device_set_wakeup_enable(dev,val); } if (n > 0) device_init_wakeup(d, v); else continue;
問題出在device_init_wakeup(d, v);
末尾的;
號,如果不允許寫這個;
號,看起來不像個函數調用,可如果寫了這個;
號,宏展開之後就有語法錯誤,if
語句被這個;
號結束掉了,沒法跟else
配對。因此,do { ... } while(0)
是一種比較好的解決辦法。
如果在一個程序檔案中重複定義一個宏,C語言規定這些重複的宏定義必須一模一樣。例如這樣的重複定義是允許的:
#define OBJ_LIKE (1 - 1) #define OBJ_LIKE /* comment */ (1/* comment */-/* comment */ 1)/* comment */
在定義的前後多些空白(空格、Tab、註釋)沒有關係,在定義之中多些空白或少些空白也沒有關係,但在定義之中有空白和沒有空白被認為是不同的,所以這樣的重複定義是不允許的:
#define OBJ_LIKE (1 - 1) #define OBJ_LIKE (1-1)
如果需要重新定義一個宏,和原來的定義不同,可以先用#undef
取消原來的定義,再重新定義,例如:
#define X 3 ... /* X is 3 */ #undef X ... /* X has no definition */ #define X 2 ... /* X is 2 */
C99引入一個新關鍵字inline
,用於定義內聯函數(inline function)。這種用法在內核代碼中很常見,例如include/linux/rwsem.h
中:
static inline void down_read(struct rw_semaphore *sem) { might_sleep(); rwsemtrace(sem,"Entering down_read"); __down_read(sem); rwsemtrace(sem,"Leaving down_read"); }
inline
關鍵字告訴編譯器,這個函數的調用要儘可能快,可以當普通的函數調用實現,也可以用宏展開的辦法實現。我們做個實驗,把上一節的例子改一下:
例 21.2. 內聯函數
inline int MAX(int a, int b) { return a > b ? a : b; } int a[] = { 9, 3, 5, 2, 1, 0, 8, 7, 6, 4 }; int max(int n) { return n == 0 ? a[0] : MAX(a[n], max(n-1)); } int main(void) { max(9); return 0; }
按往常的步驟編譯然後反彙編:
$ gcc main.c -g $ objdump -dS a.out ... int max(int n) { 8048369: 55 push %ebp 804836a: 89 e5 mov %esp,%ebp 804836c: 83 ec 0c sub $0xc,%esp return n == 0 ? a[0] : MAX(a[n], max(n-1)); 804836f: 83 7d 08 00 cmpl $0x0,0x8(%ebp) 8048373: 75 0a jne 804837f <max+0x16> 8048375: a1 c0 95 04 08 mov 0x80495c0,%eax 804837a: 89 45 fc mov %eax,-0x4(%ebp) 804837d: eb 29 jmp 80483a8 <max+0x3f> 804837f: 8b 45 08 mov 0x8(%ebp),%eax 8048382: 83 e8 01 sub $0x1,%eax 8048385: 89 04 24 mov %eax,(%esp) 8048388: e8 dc ff ff ff call 8048369 <max> 804838d: 89 c2 mov %eax,%edx 804838f: 8b 45 08 mov 0x8(%ebp),%eax 8048392: 8b 04 85 c0 95 04 08 mov 0x80495c0(,%eax,4),%eax 8048399: 89 54 24 04 mov %edx,0x4(%esp) 804839d: 89 04 24 mov %eax,(%esp) 80483a0: e8 9f ff ff ff call 8048344 <MAX> 80483a5: 89 45 fc mov %eax,-0x4(%ebp) 80483a8: 8b 45 fc mov -0x4(%ebp),%eax } ...
可以看到MAX
是作為普通函數調用的。如果指定優化選項編譯,然後反彙編:
$ gcc main.c -g -O $ objdump -dS a.out ... int max(int n) { 8048355: 55 push %ebp 8048356: 89 e5 mov %esp,%ebp 8048358: 53 push %ebx 8048359: 83 ec 04 sub $0x4,%esp 804835c: 8b 5d 08 mov 0x8(%ebp),%ebx return n == 0 ? a[0] : MAX(a[n], max(n-1)); 804835f: 85 db test %ebx,%ebx 8048361: 75 07 jne 804836a <max+0x15> 8048363: a1 a0 95 04 08 mov 0x80495a0,%eax 8048368: eb 18 jmp 8048382 <max+0x2d> 804836a: 8d 43 ff lea -0x1(%ebx),%eax 804836d: 89 04 24 mov %eax,(%esp) 8048370: e8 e0 ff ff ff call 8048355 <max> inline int MAX(int a, int b) { return a > b ? a : b; 8048375: 8b 14 9d a0 95 04 08 mov 0x80495a0(,%ebx,4),%edx 804837c: 39 d0 cmp %edx,%eax 804837e: 7d 02 jge 8048382 <max+0x2d> 8048380: 89 d0 mov %edx,%eax int a[] = { 9, 3, 5, 2, 1, 0, 8, 7, 6, 4 }; int max(int n) { return n == 0 ? a[0] : MAX(a[n], max(n-1)); } 8048382: 83 c4 04 add $0x4,%esp 8048385: 5b pop %ebx 8048386: 5d pop %ebp 8048387: c3 ret ...
可以看到,並沒有call
指令調用MAX
函數,MAX
函數的指令是內聯在max
函數中的,由於原始碼和指令的次序無法對應,max
和MAX
函數的原始碼也交錯在一起顯示。
在函數式宏定義中,#
運算符用於創建字元串,#
運算符後面應該跟一個形參(中間可以有空格或Tab),例如:
#define STR(s) # s STR(hello world)
用cpp
命令預處理之後是"hello␣world"
,自動用"
號把實參括起來成為一個字元串,並且實參中的連續多個空白字元被替換成一個空格。
再比如:
#define STR(s) #s fputs(STR(strncmp("ab\"c\0d", "abc", '\4"') == 0) STR(: @\n), s);
預處理之後是fputs("strncmp(\"ab\\\"c\\0d\", \"abc\", '\\4\"') == 0" ": @\n", s);
,注意如果實參中包含字元常量或字元串,則宏展開之後字元串的界定符"
要替換成\"
,字元常量或字元串中的\
和"
字元要替換成\\
和\"
。
在宏定義中可以用##
運算符把前後兩個預處理Token連接成一個預處理Token,和#
運算符不同,##
運算符不僅限于函數式宏定義,變數式宏定義也可以用。例如:
#define CONCAT(a, b) a##b CONCAT(con, cat)
預處理之後是concat
。再比如,要定義一個宏展開成兩個#
號,可以這樣定義:
#define HASH_HASH # ## #
中間的##
是運算符,宏展開時前後兩個#
號被這個運算符連接在一起。注意中間的兩個空格是不可少的,如果寫成####
,會被劃分成##
和##
兩個Token,而根據定義##
運算符用於連接前後兩個預處理Token,不能出現在宏定義的開頭或末尾,所以會報錯。
我們知道printf
函數帶有可變參數,函數式宏定義也可以帶可變參數,同樣是在參數列表中用...
表示可變參數。例如:
#define showlist(...) printf(#__VA_ARGS__) #define report(test, ...) ((test)?printf(#test):\ printf(__VA_ARGS__)) showlist(The first, second, and third items.); report(x>y, "x is %d but y is %d", x, y);
預處理之後變成:
printf("The first, second, and third items."); ((x>y)?printf("x>y"): printf("x is %d but y is %d", x, y));
在宏定義中,可變參數的部分用__VA_ARGS__
表示,實參中對應...
的幾個參數可以看成一個參數替換到宏定義中__VA_ARGS__
所在的地方。
調用函數式宏定義允許傳空參數,這一點和函數調用不同,通過下面幾個例子理解空參數的用法。
#define FOO() foo FOO()
預處理之後變成foo
。FOO
在定義時不帶參數,在調用時也不允許傳參數給它。
#define FOO(a) foo##a FOO(bar) FOO()
預處理之後變成:
foobar foo
FOO
在定義時帶一個參數,在調用時必須傳一個參數給它,如果不傳參數則表示傳了一個空參數。
#define FOO(a, b, c) a##b##c FOO(1,2,3) FOO(1,2,) FOO(1,,3) FOO(,,3)
預處理之後變成:
123 12 13 3
FOO
在定義時帶三個參數,在調用時也必須傳三個參數給它,空參數的位置可以空着,但必須給夠三個參數,FOO(1,2)
這樣的調用是錯誤的。
#define FOO(a, ...) a##__VA_ARGS__ FOO(1) FOO(1,2,3,)
預處理之後變成:
1 12,3,
FOO(1)
這個調用相當於可變參數部分傳了一個空參數,FOO(1,2,3,)
這個調用相當於可變參數部分傳了三個參數,第三個是空參數。
gcc
有一種擴展語法,如果##
運算符用在__VA_ARGS__
前面,除了起連接作用之外還有特殊的含義,例如內核代碼net/netfilter/nf_conntrack_proto_sctp.c
中的:
#define DEBUGP(format, ...) printk(format, ## __VA_ARGS__)
printk
這個內核函數相當於printf
,也帶有格式化字元串和可變參數,由於內核不能調用libc
的函數,所以另外實現了一個打印函數。這個函數式宏定義可以這樣調用:DEBUGP("info no. %d", 1)
。也可以這樣調用:DEBUGP("info")
。後者相當於可變參數部分傳了一個空參數,但展開後並不是printk("info",)
,而是printk("info")
,當__VA_ARGS
是空參數時,##
運算符把它前面的,
號“吃”掉了。
以上舉的宏展開的例子都是最簡單的,有些宏展開的過程要做多次替換,例如:
#define sh(x) printf("n" #x "=%d, or %d\n",n##x,alt[x]) #define sub_z 26 sh(sub_z)
sh(sub_z)
要用sh(x)
這個宏定義來展開,形參x
對應的實參是sub_z
,替換過程如下:
#x
要替換成"sub_z"
。
n##x
要替換成nsub_z
。
除了帶#
和##
運算符的參數之外,其它參數在替換之前要對實參本身做充分的展開,所以應該先把sub_z
展開成26再替換到alt[x]
中x
的位置。
現在展開成了printf("n" "sub_z" "=%d, or %d\n",nsub_z,alt[26])
,所有參數都替換完了,這時編譯器會再掃瞄一遍,再找出可以展開的宏定義來展開,假設nsub_z
或alt
是變數式宏定義,這時會進一步展開。
再舉一個例子:
#define x 3 #define f(a) f(x * (a)) #undef x #define x 2 #define g f #define t(a) a t(t(g)(0) + t)(1);
展開的步驟是:
先把g
展開成f
再替換到#define t(a) a
中,得到t(f(0) + t)(1);
。
根據#define f(a) f(x * (a))
,得到t(f(x * (0)) + t)(1);
。
把x
替換成2,得到t(f(2 * (0)) + t)(1);
。注意,一開始定義x
為3,但是後來用#undef x
取消了x
的定義,又重新定義x
為2。當處理到t(t(g)(0) + t)(1);
這一行代碼時x
已經定義成2了,所以用2來替換。還要注意一點,現在得到的t(f(2 * (0)) + t)(1);
中仍然有f
,但不能再次根據#define f(a) f(x * (a))
展開了,f(2 * (0))
就是由展開f(0)
得到的,這裡面再遇到f
就不展開了,這樣規定可以避免無窮展開(類似於無窮遞歸),因此我們可以放心地使用遞歸定義,例如#define a a[0]
,#define a a.member
等。
根據#define t(a) a
,最終展開成f(2 * (0)) + t(1);
。這時不能再展開t(1)
了,因為這裡的t
就是由展開t(f(2 * (0)) + t)
得到的,所以不能再展開了。