2. 宏定義

較大的項目都會用大量的宏定義來組織代碼,你可以看看/usr/include下面的標頭檔中用了多少個宏定義。看起來宏展開就是做個替換而已,其實裡面有比較複雜的規則,C語言有很多複雜但不常用的語法規則本書並不涉及,但有關宏展開的語法規則本節卻力圖做全面講解,因為它很重要也很常用。

2.1. 函數式宏定義

以前我們用過的#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))

就像函數調用一樣,把兩個實參分別替換到宏定義中形參ab的位置。注意這種函數式宏定義和真正的函數調用有什麼不同:

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是個真正的函數,ab只增加一次。但如果MAX是上面那樣的宏定義,則要展開成k = ((++a)>(++b)?(++a):(++b))ab就不一定是增加一次還是兩次了。

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 */

2.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函數中的,由於原始碼和指令的次序無法對應,maxMAX函數的原始碼也交錯在一起顯示。

2.3. ###運算符和可變參數

在函數式宏定義中,#運算符用於創建字元串,#運算符後面應該跟一個形參(中間可以有空格或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()

預處理之後變成fooFOO在定義時不帶參數,在調用時也不允許傳參數給它。

#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是空參數時,##運算符把它前面的,號“”掉了。

2.4. 宏展開的步驟

以上舉的宏展開的例子都是最簡單的,有些宏展開的過程要做多次替換,例如:

#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,替換過程如下:

  1. #x要替換成"sub_z"

  2. n##x要替換成nsub_z

  3. 除了帶###運算符的參數之外,其它參數在替換之前要對實參本身做充分的展開,所以應該先把sub_z展開成26再替換到alt[x]x的位置。

  4. 現在展開成了printf("n" "sub_z" "=%d, or %d\n",nsub_z,alt[26]),所有參數都替換完了,這時編譯器會再掃瞄一遍,再找出可以展開的宏定義來展開,假設nsub_zalt是變數式宏定義,這時會進一步展開。

再舉一個例子:

#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);

展開的步驟是:

  1. 先把g展開成f再替換到#define t(a) a中,得到t(f(0) + t)(1);

  2. 根據#define f(a) f(x * (a)),得到t(f(x * (0)) + t)(1);

  3. 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等。

  4. 根據#define t(a) a,最終展開成f(2 * (0)) + t(1);。這時不能再展開t(1)了,因為這裡的t就是由展開t(f(2 * (0)) + t)得到的,所以不能再展開了。