0. Overview
C的預處理指令格式為#name
,均以#
開頭,#
和指令名之間不可有空白字符,#
前可以有空字符,但為增強可讀性,一般應從第一列開始
#name
不能由宏展開得來,name
也不能由宏展開得來,如
// Wrong 1
#define INC #include
INC <stdio.h>
// Wrong 2
#define INC include
#INC <stdio.h>
預處理指令只能占一行,但是在寫代碼時可以用'\'分隔多行,但處理時仍會將這多行合為一行。有些指令帶參數,參數需與指令由空白字符分隔
預處理指令主要提供下列功能:
- 引入頭文件
- 宏展開
- 條件編譯
- line control:
#line
(感覺一般人用不着) - 診斷(diagnostics):可在編譯器檢查程序,發出errors或warnings
1. 頭文件
用#include
來包含頭文件,該指令的參數形式有兩種:
#include <file>
用於系統頭文件。Preprocessor將在a standard list of system directories下搜尋文件file
,可以用編譯器的-I
選項來將目錄添加到這個list#include "file"
用於程序自身的頭文件。Preprocessor的搜尋順序如下:
a. 先在包含該文件的當前目錄搜尋文件file
,
b. 然后在quote directories中搜尋,可以用編譯器的-iquote
選項來將目錄添加到quote directories中
c. 最后再在用於搜尋<file>
的目錄下搜尋(即 1. 中的順序)(所以用#include "stdio.h"
只要你不覆蓋這個頭文件的話也不會出現問題,總能找着)
#include
的參數無論是用""
還是<>
括起來,都如同一個字符串,里面的注釋不會被識別,宏也不會展開。但是不同於字符串的是,backslash不再有轉義作用,而是一個單純的字符'\'
在這一行,文件名參數后面除了注釋外不能有任何其他內容
只包含一次 Once-Only Headers
如果一個頭文件被include兩次,編譯器就會處理兩次,因此可能會出錯,如重定義等等,標准做法是用所謂的wrapper #ifndef
將頭文件的內容包起來,如:
/* File foo. */
#ifndef FILE_FOO_SEEN
#define FILE_FOO_SEEN
the entire file
#endif /* !FILE_FOO_SEEN */
代碼片段中的宏FILE_FOO_SEEN
叫做controlling macro
或者guard macro
,在用戶程序頭文件中,該宏的名字不能以_
開頭,在系統頭文件中,該宏的名字需要以__
(雙下划線)開頭以免與用戶程序頭文件沖突。在任意類型的頭文件中,該宏的名字應該包含頭文件文件名再加上額外的文字以避免與其他頭文件沖突
2. 宏 Macros
宏是賦予名字的一段代碼,每次使用時都將名字替換成宏內容。宏分為兩種,它們在使用時有很大的不同:
- Object-like macros:使用時像用data objects一樣
- Function-like macros:使用時像函數調用一樣
2.1 對象形式的宏 Object-like macros
Object-like macro就是一個簡單的標志符,表示一個代碼片段,在使用時由這個代碼片段來替換,用法:
#define NAME macro_body
宏body又叫expansion或replacement list,是一個token序列
按照慣例,宏的名字一般用大寫字母
#define macro_body
也只占一行,並且macro_body
后面不能有其他內容(除空白字符或注釋外),在寫代碼時也可以用'\'分隔多行,但預處理時仍會將它們合為一行
C preprocessor順序地掃描源程序,因此宏定義只從定義處開始生效
宏展開是遞歸進行的,preprocessor將一個宏展開后會接着處理展開后的結果,如果這里面有其他的宏,會繼續展開下去。但是如果結果里面再次出現剛剛展開的這個宏的話將不會展開第二次,以免出現無限遞歸的情況
#define TABLESIZE BUFSIZE
#define BUFSIZE 1024
TABLESIZE
// -> 先展開為 BUFSIZE
// -> 再展開為 1024
注意雖然宏會展開多次,但是每次的展開過程只是單純地用body替換name,如上面的例子中在展開TABLESIZE
時只是單純地用BUFSIZE
來替換它,接下來preprocessor才檢查替換結果是不是另一個宏
2.2 函數形式的宏 Function-like macros
如其名,這種宏使用起來像函數調用一樣。用法:
#define name() body
注意,小括號()
必須和宏的名字連在一起,否則會被當成object-like宏來展開,同時,在使用時也必須用name()
的形式(此時name
和()
間可以有空格,2.3中同),只用name
的話不會被展開
2.3 宏參數
Function-like宏像函數一樣可以接受參數,用法:
#define name(params_list)
其中params_list
是參數列表,參數必須是有效的C標志符,由,
分隔(參數列表中可以出現空格,但是空格沒有實際作用)
在“調用”函數形式的宏時,將實參列表寫在宏name后面的小括號里,由,
分隔,函數形式宏的“調用”不限制在一行內,可以寫成多行,但是參數數量必須和定義時的數量相匹配。可以實參可以是空,但是數量也必須匹配(直白講即逗號數量必須一致),如:
min(, b) → (( ) < (b) ? ( ) : (b))
min(a, ) → ((a ) < ( ) ? (a ) : ( ))
min(,) → (( ) < ( ) ? ( ) : ( ))
min((,),) → (((,)) < ( ) ? ((,)) : ( ))
min() error→ macro "min" requires 2 arguments, but only 1 given
min(,,) error→ macro "min" passed 3 arguments, but takes just 2
如
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
x = min(a, b); → x = ((a) < (b) ? (a) : (b));
y = min(1, 2); → y = ((1) < (2) ? (1) : (2));
z = min(a + 28, *p); → z = ((a + 28) < (*p) ? (a + 28) : (*p));
在展開時會去除各個實參的leading、trailing whitespace,實參的token序列中的whitespace會減成一個空格。在每個實參中,小括號必須平衡,小括號中的逗號不會結束這個參數(即小括號中的逗號不是實參分隔符),但中括號和大括號不要求平衡,而且它們中的逗號會作為實參分隔符截斷這個參數
宏定義中若參數出現在字符串中,在展開時不會展開成相應實參,如:
#define foo(x) x, "x"
foo(bar) → bar, "x"
2.4 字符串化 Stringizing
有時可能需要講宏參數轉換成字符串常量,但是在 2.3 的最后提到字符串中的參數不會被實參替換,為了解決這個問題,可以用預處理操作符#
來進行轉換。當參數有一個前導#
時,preprocessor會將其替換為實參,再轉換成字符串常量,但是這個過程發生后,被轉換成的字符串中如果還有宏則不會繼續展開,如果還想繼續展開,則需要寫成多級宏的形式,如:
#define xstr(s) str(s)
#define str(s) #s
#define foo 4
str (foo)
→ "foo"
xstr (foo)
→ xstr (4)
→ str (4)
→ "4"
2.5 拼接
預處理操作符##
用於在宏body中將兩個tokens拼在一起,如A ## B
將展開為AB
,要求展開后必須是一個有效的C標志符,如一個標志符和數字拼接,兩個數字間的拼接,一些復合操作符如+=
的拼接等等,有些拼接是無效的,如x
和+
。
拼接常見的應用場景為宏參數間的拼接,如:
#define COMMAND(NAME) { #NAME, NAME ## _command }
struct command commands[] =
{
COMMAND (quit),
COMMAND (help),
…
};
2.6 取消宏定義
#undef name
用於取消宏定義,name
可以是object-like宏的名字,或者是function-like宏的名字(不用加小括號以及參數列表)
3. 條件編譯
3.1 條件編譯常用場景
- 根據機器架構或操作系統的不同使用不同的代碼
- 將原文件編譯成兩個不同的程序,其中一個版本可能會用於輸出一些data進行debugging等等
- 使用
#if 0
來將排除一段代碼,但將其保留在源文件中用作注釋
3.2 條件編譯語法
ifdef
,ifndef
#ifdef MACRO
controlled text
#endif /* MACRO */
if
#if expression
controlled text
#endif /* expression */
expression
是一個integer類型的C表達式,可以包含
- 整形常量
- 字符常量
- 數學運算表達式和邏輯運算表達式(遵循短路求值)
- 宏,在計算宏所代表的表達式前將先展開所有的宏
defined
預處理指令- 所有不是宏的標志符都視為數字0,函數形式的宏但沒有調用實參列表也視為0
defined
用在#if
和#elif
表達式中,用於測試一個名字是否被定義成了一個宏,defined name
和defiend ( name )
作用相同:如果name
定義為了一個宏,則表達式值為1,否則為0,因此#if defined MACRO
等價於#ifdef MACRO
在測試多個宏是否存在時defined
比較有用,如:
#if defined (__vax__) || defined (__ns16000__)
else
可以用在#if
、#ifdef
、#ifndef
中
elif
elif
不需要一個#endif
和其匹配
4. 診斷信息
#error
導致preprocessor產生一個fatal error,#error
所在行的剩余tokens組成錯誤信息#warning
導致preprocessor產生一個warning並繼續預處理,#warning
所在行的剩余tokens組成錯誤信息
兩者都不對其參數進行宏展開