# 第?21?章?預處理
**目錄**
+ [1\. 預處理的步驟](ch21s01.html)
+ [2\. 宏定義](ch21s02.html)
+ [2.1\. 函數式宏定義](ch21s02.html#id2797214)
+ [2.2\. 內聯函數](ch21s02.html#id2797661)
+ [2.3\. `#`、`##`運算符和可變參數](ch21s02.html#id2797840)
+ [2.4\. 宏展開的步驟](ch21s02.html#id2798306)
+ [3\. 條件預處理指示](ch21s03.html)
+ [4\. 其它預處理特性](ch21s04.html)
## 1.?預處理的步驟
現在我們全面了解一下C編譯器做語法解析之前的預處理步驟:
1、把[第?2?節 “常量”](ch02s02.html#expr.constant)提到過的三連符替換成相應的單字符。
2、把用`\`字符續行的多行代碼接成一行。例如:
```
#define STR "hello, "\
"world"
```
經過這個預處理步驟之后接成一行:
```
#define STR "hello, " "world"
```
這種續行的寫法要求`\`后面緊跟換行,中間不能有其它空白字符。
3、把注釋(不管是單行注釋還是多行注釋)都替換成一個空格。
4、經過以上兩步之后去掉了一些換行,有的換行在續行過程中去掉了,有的換行在多行注釋之中,也隨著注釋一起去掉了,剩下的代碼行稱為邏輯代碼行。然后預處理器把邏輯代碼行劃分成Token和空白字符,這時的Token稱為預處理Token,包括標識符、整數常量、浮點數常量、字符常量、字符串、運算符和其它符號。繼續上面的例子,兩個源代碼行被接成一個邏輯代碼行,然后這個邏輯代碼行被劃分成Token和空白字符:`#`,`define`,空格,`STR`,空格,`"hello, "`,Tab,Tab,`"world"`。
5、在Token中識別出預處理指示,做相應的預處理動作,如果遇到`#include`預處理指示,則把相應的源文件包含進來,并對源文件做以上1-4步預處理。如果遇到宏定義則做宏展開。
我們早在[第?2?節 “數組應用實例:統計隨機數”](ch08s02.html#array.statistic)就認識了預處理指示這個概念,現在給出它的嚴格定義。一條預處理指示由一個邏輯代碼行組成,以`#`開頭,后面跟若干個預處理Token,在預處理指示中允許使用的空白字符只有空格和Tab。
6、找出字符常量或字符串中的轉義序列,用相應的字節來替換它,比如把`\n`替換成字節0x0a。
7、把相鄰的字符串連接起來。繼續上面的例子,如果代碼中有:
```
printf(
STR);
```
經過第4步處理劃分成以下Token:`printf`,`(`,換行,Tab,`STR`,`)`,`;`,換行。經過第5步宏展開后變成以下Token:`printf`,`(`,換行,Tab,`"hello, "`,Tab,Tab,`"world"`,`)`,`;`,換行。然后把相鄰的字符串連接起來,變成以下Token:`printf`,`(`,換行,Tab,`"hello, world"`,`)`,`;`,換行。
8、經過以上處理之后,把空白字符丟掉,把Token交給C編譯器做語法解析,這時就不再是預處理Token,而稱為C Token了。這里丟掉的空白字符包括空格、換行、水平Tab、垂直Tab、分頁符。繼續上面的例子,最后交給C編譯器做語法解析的Token是:`printf`,`(`,`"hello, world"`,`)`,`;`。注意,把一個預處理指示寫成多行要用`\`續行,因為根據定義,一條預處理指示只能由一個邏輯代碼行組成,而把C代碼寫成多行則不需要用`\`續行,因為換行在C代碼中只不過是一種空白字符,在做語法解析時所有空白字符都已經丟掉了。
## 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))
```
就像函數調用一樣,把兩個實參分別替換到宏定義中形參`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 */
```
### 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`函數中的,由于源代碼和指令的次序無法對應,`max`和`MAX`函數的源代碼也交錯在一起顯示。
### 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()
```
預處理之后變成`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`是空參數時,`##`運算符把它前面的`,`號“吃”掉了。
### 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_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);
```
展開的步驟是:
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)`得到的,所以不能再展開了。
## 3.?條件預處理指示
我們在[第?2.2?節 “頭文件”](ch20s02.html#link.header)中見過Header Guard的用法:
```
#ifndef HEADER_FILENAME
#define HEADER_FILENAME
/* body of header */
#endif
```
條件預處理指示也常用于源代碼的配置管理,例如:
```
#if MACHINE == 68000
int x;
#elif MACHINE == 8086
long x;
#else /* all others */
#error UNKNOWN TARGET MACHINE
#endif
```
假設這段程序是為多種平臺編寫的,在68000平臺上需要定義`x`為`int`型,在8086平臺上需要定義`x`為`long`型,對其它平臺暫不提供支持,就可以用條件預處理指示來寫。如果在預處理這段代碼之前,`MACHINE`被定義為68000,則包含`intx;`這段代碼;否則如果`MACHINE`被定義為8086,則包含`long x;`這段代碼;否則(`MACHINE`沒有定義,或者定義為其它值),包含`#error UNKNOWN TARGET MACHINE`這段代碼,編譯器遇到這個預處理指示就報錯退出,錯誤信息就是`UNKNOWN TARGET MACHINE`。
如果要為8086平臺編譯這段代碼,有幾種可選的辦法:
1、手動編輯代碼,在前面添一行`#define MACHINE 8086`。這樣做的缺點是難以管理,如果這個項目中有很多源文件都需要定義`MACHINE`,每次要為8086平臺編譯就得把這些定義全部改成8086,每次要為68000平臺編譯就得把這些定義全部改成68000。
2、在所有需要配置的源文件開頭包含一個頭文件,在頭文件中定義`#define MACHINE 8086`,這樣只需要改一個頭文件就可以影響所有包含它的源文件。通常這個頭文件由配置工具生成,比如在Linux內核源代碼的目錄下運行`make menuconfig`命令可以出來一個配置菜單,在其中配置的選項會自動轉換成頭文件`include/linux/autoconf.h`中的宏定義。
舉一個具體的例子,在內核配置菜單中用回車鍵和方向鍵進入`Device Drivers ---> Network device support`,然后用空格鍵選中`Network device support`(菜單項左邊的`[ ]`括號內會出現一個`*`號),然后保存退出,會生成一個名為`.config`的隱藏文件,其內容類似于:
```
...
#
# Network device support
#
CONFIG_NETDEVICES=y
# CONFIG_DUMMY is not set
# CONFIG_BONDING is not set
# CONFIG_EQUALIZER is not set
# CONFIG_TUN is not set
...
```
然后運行`make`命令編譯內核,這時根據`.config`文件生成頭文件`include/linux/autoconf.h`,其內容類似于:
```
...
/*
* Network device support
*/
#define CONFIG_NETDEVICES 1
#undef CONFIG_DUMMY
#undef CONFIG_BONDING
#undef CONFIG_EQUALIZER
#undef CONFIG_TUN
...
```
上面的代碼用`#undef`確保取消一些宏的定義,如果先前沒有定義過`CONFIG_DUMMY`,用`#undef CONFIG_DUMMY`取消它的定義沒有任何作用,也不算錯。
`include/linux/autoconf.h`被另一個頭文件`include/linux/config.h`所包含,通常內核代碼包含后一個頭文件,例如`net/core/sock.c`:
```
...
#include <linux/config.h>
...
int sock_setsockopt(struct socket *sock, int level, int optname,
char __user *optval, int optlen)
{
...
#ifdef CONFIG_NETDEVICES
case SO_BINDTODEVICE:
{
...
}
#endif
...
```
再比如`drivers/isdn/i4l/isdn_common.c`:
```
...
#include <linux/config.h>
...
static int
isdn_ioctl(struct inode *inode, struct file *file, uint cmd, ulong arg)
{
...
#ifdef CONFIG_NETDEVICES
case IIOCNETGPN:
/* Get peer phone number of a connected
* isdn network interface */
if (arg) {
if (copy_from_user(&phone, argp, sizeof(phone)))
return -EFAULT;
return isdn_net_getpeer(&phone, argp);
} else
return -EINVAL;
#endif
...
#ifdef CONFIG_NETDEVICES
case IIOCNETAIF:
...
#endif /* CONFIG_NETDEVICES */
...
```
這樣,在配置菜單中所做的配置通過條件預處理最終決定了哪些代碼被編譯到內核中。`#ifdef`或`#if`可以嵌套使用,但預處理指示通常都頂頭寫不縮進,為了區分嵌套的層次,可以像上面的代碼中最后一行那樣,在`#endif`處用注釋寫清楚它結束的是哪個`#if`或`#ifdef`。
3、要定義一個宏不一定非得在代碼中用`#define`定義,早在[第?6?節 “折半查找”](ch11s06.html#sortsearch.binary)我們就見過用`gcc`的`-D`選項定義一個宏`NDEBUG`。對于上面的例子,我們需要給`MACHINE`定義一個值,可以寫成類似這樣的命令:`gcc -c -DMACHINE=8086 main.c`。這種辦法需要給每個編譯命令都加上適當的選項,和第2種方法相比似乎也很麻煩,第2種方法在頭文件中只寫一次宏定義就可以在很多源文件中生效,第3種方法能不能做到“只寫一次到處生效”呢?等以后學習了Makefile就有辦法了。
最后通過下面的例子說一下`#if`后面的表達式:
```
#define VERSION 2
#if defined x || y || VERSION < 3
```
1. 首先處理`defined`運算符,`defined`運算符一般用作表達式中的一部分,如果單獨使用,`#if defined x`相當于`#ifdef x`,而`#if !defined x`相當于`#ifndef x`。在這個例子中,如果`x`這個宏有定義,則把`defined x`替換為1,否則替換為0,因此變成`#if 0 || y || VERSION < 3`。
2. 然后把有定義的宏展開,變成`#if 0 || y || 2 < 3`。
3. 把沒有定義的宏替換成0,變成`#if 0 || 0 || 2 < 3`,注意,即使前面定義了一個變量名是`y`,在這一步也還是替換成0,因為`#if`的表達式必須在編譯時求值,其中包含的名字只能是宏定義。
4. 把得到的表達式`0 || 0 || 2 < 3`像C表達式一樣求值,求值的結果是`#if 1`,因此條件成立。
## 4.?其它預處理特性
`#pragma`預處理指示供編譯器實現一些非標準的特性,C標準沒有規定`#pragma`后面應該寫什么以及起什么作用,由編譯器自己規定。有的編譯器用`#pragma`定義一些特殊功能寄存器名,有的編譯器用`#pragma`定位鏈接地址,本書不做深入討論。如果編譯器在代碼中碰到不認識的`#pragma`指示則忽略它,例如`gcc`的`#pragma`指示都是`#pragma GCC ...`這種形式,用別的編譯器編譯則忽略這些指示。
C標準規定了幾個特殊的宏,在不同的地方使用可以自動展開成不同的值,常用的有`__FILE__`和`__LINE__`,`__FILE__`展開為當前源文件的文件名,是一個字符串,`__LINE__`展開為當前代碼行的行號,是一個整數。這兩個宏在源代碼中不同的位置使用會自動取不同的值,顯然不是用`#define`能定義得出來的,它們是編譯器內建的特殊的宏。在打印調試信息時打印這兩個宏可以給開發者非常有用的提示,例如在[第?6?節 “折半查找”](ch11s06.html#sortsearch.binary)我們看到`assert`函數打印的錯誤信息就有`__FILE__`和`__LINE__`的值。現在我們自己實現這個`assert`函數,以理解它的原理。這個實現出自[[Standard C Library]](bi01.html#bibli.standardclib "The Standard C Library"):
**例?21.3.?assert.h的一種實現**
```
/* assert.h standard header */
#undef assert /* remove existing definition */
#ifdef NDEBUG
#define assert(test) ((void)0)
#else /* NDEBUG not defined */
void _Assert(char *);
/* macros */
#define _STR(x) _VAL(x)
#define _VAL(x) #x
#define assert(test) ((test) ? (void)0 \
: _Assert(__FILE__ ":" _STR(__LINE__) " " #test))
#endif
```
通過這個例子可以全面復習本章所講的知識。C標準規定`assert`應該實現為宏定義而不是一個真正的函數,并且`assert(test)`這個表達式的值應該是`void`類型的。首先用`#undef assert`確保取消前面對`assert`的定義,然后分兩種情況:如果定義了`NDEBUG`,那么`assert(test)`直接定義成一個`void`類型的值,什么也不做;如果沒有定義`NDEBUG`,則要判斷測試條件`test`是否成立,如果條件成立就什么也不做,如果不成立則調用`_Assert`函數。假設在`main.c`文件的第`33`行調用`assert(is_sorted())`,那么`__FILE__`是字符串`"main.c"`,`__LINE__`是整數`33`,`#test`是字符串`"is_sorted()"`。注意`_STR(__LINE__)`的展開過程:首先展開成`_VAL(33)`,然后進一步展開成字符串`"33"`。這樣,最后`_Assert`調用的形式是`_Assert("main.c" ":" "33" " " "is_sorted()")`,傳給`_Assert`函數的字符串是`"main.c:33 is_sorted()"`。`_Assert`函數是我們自己定義的,在另一個源文件中:
```
/* xassert.c _Assert function */
#include <stdio.h>
#include <stdlib.h>
void _Assert(char *mesg)
{ /* print assertion message and abort */
fputs(mesg, stderr);
fputs(" -- assertion failed\n", stderr);
abort();
}
```
注意,在頭文件`assert.h`中自己定義的內部使用的標識符都以`_`線開頭,例如`_STR`,`_VAL`,`_Assert`,因為我們在模擬C標準庫的實現,在[第?3?節 “變量”](expr.variable.html "3.?變量")講過,以`_`線開頭的標識符通常由編譯器和C語言庫使用,在`/usr/include`下的頭文件中你可以看到大量`_`線開頭的標識符。另外一個問題,為什么我們不直接在`assert`的宏定義中調用`fputs`和`abort`呢?因為調用這兩個函數需要包含`stdio.h`和`stdlib.h`,C標準庫的頭文件應該是相互獨立的,一個程序只要包含`assert.h`就應該能使用`assert`,而不應該再依賴于別的頭文件。`_Assert`中的`fputs`向標準錯誤輸出打印錯誤信息,`abort`異常終止當前進程,這些函數以后再詳細討論。
現在測試一下我們的`assert`實現,把`assert.h`和`xassert.c`和測試代碼`main.c`放在同一個目錄下。
```
/* main.c */
#include "assert.h"
int main(void)
{
assert(2>3);
return 0;
}
```
注意`#include "assert.h"`要用`"`引號而不要用`<>`括號,以保證包含的是我們自己寫的`assert.h`而非C標準庫的頭文件。然后編譯運行:
```
$ gcc main.c xassert.c
$ ./a.out
main.c:6 2>3 -- assertion failed
Aborted
```
在打印調試信息時除了文件名和行號之外還可以打印出當前函數名,C99引入一個特殊的標識符`__func__`支持這一功能。這個標識符應該是一個變量名而不是宏定義,不屬于預處理的范疇,但它的作用和`__FILE__`、`__LINE__`類似,所以放在一起講。例如:
**例?21.4.?特殊標識符__func__**
```
#include <stdio.h>
void myfunc(void)
{
printf("%s\n", __func__);
}
int main(void)
{
myfunc();
printf("%s\n", __func__);
return 0;
}
```
```
$ gcc main.c
$ ./a.out
myfunc
main
```
- Linux C編程一站式學習
- 歷史
- 前言
- 部分?I.?C語言入門
- 第?1?章?程序的基本概念
- 第?2?章?常量、變量和表達式
- 第?3?章?簡單函數
- 第?4?章?分支語句
- 第?5?章?深入理解函數
- 第?6?章?循環語句
- 第?7?章?結構體
- 第?8?章?數組
- 第?9?章?編碼風格
- 第?10?章?gdb
- 第?11?章?排序與查找
- 第?12?章?棧與隊列
- 第?13?章?本階段總結
- 部分?II.?C語言本質
- 第?14?章?計算機中數的表示
- 第?15?章?數據類型詳解
- 第?16?章?運算符詳解
- 第?17?章?計算機體系結構基礎
- 第?18?章?x86匯編程序基礎
- 第?19?章?匯編與C之間的關系
- 第?20?章?鏈接詳解
- 第?21?章?預處理
- 第?22?章?Makefile基礎
- 第?23?章?指針
- 第?24?章?函數接口
- 第?25?章?C標準庫
- 第?26?章?鏈表、二叉樹和哈希表
- 第?27?章?本階段總結
- 部分?III.?Linux系統編程
- 第?28?章?文件與I/O
- 第?29?章?文件系統
- 第?30?章?進程
- 第?31?章?Shell腳本
- 第?32?章?正則表達式
- 第?33?章?信號
- 第?34?章?終端、作業控制與守護進程
- 第?35?章?線程
- 第?36?章?TCP/IP協議基礎
- 第?37?章?socket編程
- 附錄?A.?字符編碼
- 附錄?B.?GNU Free Documentation License Version 1.3, 3 November 2008
- 參考書目
- 索引