導語:C初學者可能對前處理器沒什麼概念, 這是情有可原,下面是C中的預編譯巨集定義,一起來學習下吧:
(一) 預處理命令簡介
預處理命令由#(hash字元)開頭, 它獨佔一行, #之前只能是空白符. 以#開頭的語句就是預處理命令, 不以#開頭的語句為C中的程式碼行. 常用的預處理命令如下:
#define 定義一個預處理巨集
#undef 取消巨集的定義
#include 包含檔案命令
#include_next 與#include相似, 但它有著特殊的用途
#if 編譯預處理中的條件命令, 相當於C語法中的if語句
#ifdef 判斷某個巨集是否被定義, 若已定義, 執行隨後的語句
#ifndef 與#ifdef相反, 判斷某個巨集是否未被定義
#elif 若#if, #ifdef, #ifndef或前面的#elif條件不滿足, 則執行#elif之後的語句, 相當於C語法中的else-if
#else 與#if, #ifdef, #ifndef對應, 若這些條件不滿足, 則執行#else之後的語句, 相當於C語法中的else
#endif #if, #ifdef, #ifndef這些條件命令的結束標誌.
defined 與#if, #elif配合使用, 判斷某個巨集是否被定義
#line 標誌該語句所在的行號
# 將巨集引數替代為以引數值為內容的字元竄常量
## 將兩個相鄰的標記(token)連線為一個單獨的標記
#pragma 說明編譯器資訊
#warning 顯示編譯警告資訊
#error 顯示編譯錯誤資訊
(二) 預處理的文法
預處理並不分析整個原始碼檔案, 它只是將原始碼分割成一些標記(token), 識別語句中哪些是C語句, 哪些是預處理語句. 前處理器能夠識別C標記, 檔名, 空白符, 檔案結尾標誌.
預處理語句格式: #command name(...) token(s)
1, command預處理命令的名稱, 它之前以#開頭, #之後緊隨預處理命令, 標準C允許#兩邊可以有空白符, 但比較老的編譯器可能不允許這樣. 若某行中只包含#(以及空白符), 那麼在標準C中該行被理解為空白. 整個預處理語句之後只能有空白符或者註釋, 不能有其它內容.
2, name代表巨集名稱, 它可帶引數. 引數可以是可變引數列表(C99).
3, 語句中可以利用""來換行.
e.g.
# define ONE 1 /* ONE == 1 */
等價於: #define ONE 1
#define err(flag, msg) if(flag)
printf(msg)
等價於: #define err(flag, msg) if(flag) printf(msg)
(三) 預處理命令詳述
1, #define
#define命令定義一個巨集:
#define MACRO_NAME(args) tokens(opt)
之後出現的MACRO_NAME將被替代為所定義的標記(tokens). 巨集可帶引數, 而後面的標記也是可選的.
物件巨集
不帶引數的巨集被稱為"物件巨集(objectlike macro)"
#define經常用來定義常量, 此時的巨集名稱一般為大寫的字串. 這樣利於修改這些常量.
e.g.
#define MAX 100
int a[MAX];
#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif
#define __FILE_H__ 中的巨集就不帶任何引數, 也不擴充套件為任何標記. 這經常用於包含標頭檔案.
要呼叫該巨集, 只需在程式碼中指定巨集名稱, 該巨集將被替代為它被定義的內容.
函式巨集
帶引數的巨集也被稱為"函式巨集". 利用巨集可以提高程式碼的執行效率: 子程式的呼叫需要壓棧出棧, 這一過程如果過於頻繁會耗費掉大量的CPU運算資源. 所以一些程式碼量小但執行頻繁的程式碼如果採用帶引數巨集來實現會提高程式碼的執行效率.
函式巨集的引數是固定的情況
函式巨集的定義採用這樣的方式: #define name( args ) tokens
其中的args和tokens都是可選的. 它和物件巨集定義上的區別在於巨集名稱之後不帶括號.
注意, name之後的左括號(必須緊跟name, 之間不能有空格, 否則這就定義了一個物件巨集, 它將被替換為 以(開始的字串. 但在呼叫函式巨集時, name與(之間可以有空格.
e.g.
#define mul(x,y) ((x)*(y))
注意, 函式巨集之後的引數要用括號括起來, 看看這個例子:
e.g.
#define mul(x,y) x*y
"mul(1, 2+2);" 將被擴充套件為: 1*2 + 2
同樣, 整個標記串也應該用括號引用起來:
e.g.
#define mul(x,y) (x)*(y)
sizeof mul(1,2.0) 將被擴充套件為 sizeof 1 * 2.0
呼叫函式巨集時候, 傳遞給它的引數可以是函式的返回值, 也可以是任何有意義的語句:
e.g.
mul (f(a,b), g(c,d));
e.g.
#define (stmt) stmt
( a=1; b=2;) 相當於在程式碼中加入 a=1; b=2 .
( a=1, b=2;) 就有問題了: 前處理器會提示出錯: 函式巨集的引數個數不匹配. 前處理器把","視為引數間的分隔符.
((a=1, b=2;)) 可解決上述問題.
在定義和呼叫函式巨集時候, 要注意一些問題:
1, 我們經常用{}來引用函式巨集被定義的內容, 這就要注意呼叫這個函式巨集時的";"問題.
example_3.7:
#define swap(x,y) { unsigned long _temp=x; x=y; y=_tmp}
如果這樣呼叫它: "swap(1,2);" 將被擴充套件為: { unsigned long _temp=1; 1=2; 2=_tmp};
明顯後面的;是多餘的, 我們應該這樣呼叫: swap(1,2)
雖然這樣的呼叫是正確的, 但它和C語法相悖, 可採用下面的方法來處理被{}括起來的內容:
#define swap(x,y)
do { unsigned long _temp=x; x=y; y=_tmp} while (0)
swap(1,2); 將被替換為:
do { unsigned long _temp=1; 1=2; 2=_tmp} while (0);
在Linux核心原始碼中對這種do-while(0)語句有這廣泛的應用.
2, 有的函式巨集是無法用do-while(0)來實現的, 所以在呼叫時不能帶上";", 最好在呼叫後添加註釋說明.
eg_3.8:
#define incr(v, low, high)
for ((v) = (low),; (v) <= (high); (v)++)
只能以這樣的形式被呼叫: incr(a, 1, 10) /* increase a form 1 to 10 */
函式巨集中的引數包括可變引數列表的情況
C99標準中新增了可變引數列表的內容. 不光是函式, 函式巨集中也可以使用可變引數列表.
#define name(args, ...) tokens
#define name(...) tokens
"..."代表可變引數列表, 如果它不是僅有的引數, 那麼它只能出現在引數列表的最後. 呼叫這樣的函式巨集時, 傳遞給它的引數個數要不少於引數列表中引數的個數(多餘的引數被丟棄).
通過__VA_ARGS__來替換函式巨集中的可變引數列表. 注意__VA_ARGS__只能用於函式巨集中引數中包含有"..."的情況.
e.g.
#ifdef DEBUG
#define my_printf(...) fprintf(stderr, __VA_ARGS__)
#else
#define my_printf(...) printf(__VA_ARGS__)
#endif
tokens中的__VA_ARGS__被替換為函式巨集定義中的'"..."可變引數列表.
注意在使用#define時候的一些常見錯誤:
#define MAX = 100
#define MAX 100;
=, ; 的使用要值得注意. 再就是呼叫函式巨集是要注意, 不要多給出";".
注意: 函式巨集對引數型別是不敏感的, 你不必考慮將何種資料型別傳遞給巨集. 那麼, 如何構建對引數型別敏感的巨集呢? 參考本章的第九部分, 關於"##"的介紹.
關於定義巨集的另外一些問題
(1) 巨集可以被多次定義, 前提是這些定義必須是相同的. 這裡的"相同"要求先後定義中空白符出現的位置相同, 但具體的空白符型別或數量可不同, 比如原先的空格可替換為多個其他型別的空白符: 可為tab, 註釋...
e.g.
#define NULL 0
#define NULL /* null pointer */ 0
上面的重定義是相同的, 但下面的重定義不同:
#define fun(x) x+1
#define fun(x) x + 1 或: #define fun(y) y+1
如果多次定義時, 再次定義的巨集內容是不同的, gcc會給出"NAME redefined"警告資訊.
應該避免重新定義函式巨集, 不管是在預處理命令中還是C語句中, 最好對某個物件只有單一的定義. 在gcc中, 若巨集出現了重定義, gcc會給出警告.
(2) 在gcc中, 可在命令列中指定物件巨集的定義:
e.g.
$ gcc -Wall -DMAX=100 -o tmp tmp.c
相當於在tmp.c中新增" #define MAX 100".
那麼, 如果原先tmp.c中含有MAX巨集的定義, 那麼再在gcc呼叫命令中使用-DMAX, 會出現什麼情況呢?
---若-DMAX=1, 則正確編譯.
---若-DMAX的值被指定為不為1的值, 那麼gcc會給出MAX巨集被重定義的警告, MAX的值仍為1.
注意: 若在呼叫gcc的命令列中不顯示地給出物件巨集的值, 那麼gcc賦予該巨集預設值(1), 如: -DVAL == -DVAL=1
(3) #define所定義的巨集的作用域
巨集在定義之後才生效, 若巨集定義被#undef取消, 則#undef之後該巨集無效. 並且字串中的巨集不會被識別
e.g.
#define ONE 1
sum = ONE + TWO /* sum = 1 + TWO */
#define TWO 2
sum = ONE + TWO /* sum = 1 + 2 */
#undef ONE
sum = ONE + TWO /* sum = ONE + 2 */
char c[] = "TWO" /* c[] = "TWO", NOT "2"! */
(4) 巨集的替換可以是遞迴的, 所以可以巢狀定義巨集.
e.g.
# define ONE NUMBER_1
# define NUMBER_1 1
int a = ONE /* a = 1 */
2, #undef
#undef用來取消巨集定義, 它與#define對立:
#undef name
如夠被取消的巨集實際上沒有被#define所定義, 針對它的#undef並不會產生錯誤.
當一個巨集定義被取消後, 可以再度定義它.
3, #if, #elif, #else, #endif
#if, #elif, #else, #endif用於條件編譯:
#if 常量表達式1
語句...
#elif 常量表達式2
語句...
#elif 常量表達式3
語句...
...
#else
語句...
#endif
#if和#else分別相當於C語句中的if, else. 它們根據常量表達式的值來判別是否執行後面的語句. #elif相當於C中的else-if. 使用這些條件編譯命令可以方便地實現對原始碼內容的控制.
else之後不帶常量表達式, 但若包含了常量表達式, gcc只是給出警告資訊.
使用它們可以提升程式碼的可移植性---針對不同的平臺使用執行不同的語句. 也經常用於大段程式碼註釋.
e.g.
#if 0
{
一大段程式碼;
}
#endif
常量表達式可以是包含巨集, 算術運算, 邏輯運算等等的合法C常量表達式, 如果常量表達式為一個未定義的巨集, 那麼它的值被視為0.
#if MACRO_NON_DEFINED == #if 0
在判斷某個巨集是否被定義時, 應當避免使用#if, 因為該巨集的值可能就是被定義為0. 而應當使用下面介紹的#ifdef或#ifndef.
注意: #if, #elif, #else之後的巨集只能是物件巨集. 如果name為名的巨集未定義, 或者該巨集是函式巨集. 那麼在gcc中使用"-Wundef"選項會顯示巨集未定義的警告資訊.
4, #ifdef, #ifndef, defined.
#ifdef, #ifndef, defined用來測試某個巨集是否被定義
#ifdef name 或 #ifndef name
它們經常用於避免標頭檔案的重複引用:
#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif
defined(name): 若巨集被定義,則返回1, 否則返回0.
它與#if, #elif, #else結合使用來判斷巨集是否被定義, 乍一看好像它顯得多餘, 因為已經有了#ifdef和#ifndef. defined用於在一條判斷語句中宣告多個判別條件:
#if defined(VAX) && defined(UNIX) && !defined(DEBUG)
和#if, #elif, #else不同, #indef, #ifndef, defined測試的巨集可以是物件巨集, 也可以是函式巨集. 在gcc中使用"-Wundef"選項不會顯示巨集未定義的警告資訊.
5, #include , #include_next
#include用於檔案包含. 在#include 命令所在的行不能含有除註釋和空白符之外的其他任何內容.
#include "headfile"
#include
#include 預處理標記
前面兩種形式大家都很熟悉, "#include 預處理標記"中, 預處理標記會被前處理器進行替換, 替換的結果必須符合前兩種形式中的某一種.
實際上, 真正被新增的標頭檔案並不一定就是#include中所指定的檔案. #include"headfile"包含的標頭檔案當然是同一個檔案, 但#include 包包含的"系統標頭檔案"可能是另外的檔案. 但這不值得被注意. 感興趣的話可以檢視巨集擴充套件後到底引入了哪些系統標頭檔案.
關於#include "headfile"和#include 的區別以及如何在gcc中包含標頭檔案的詳細資訊, 參考本blog的GCC筆記.
相對於#include, 我們對#include_next不太熟悉. #include_next僅用於特殊的場合. 它被用於標頭檔案中(#include既可用於標頭檔案中, 又可用於.c檔案中)來包含其他的標頭檔案. 而且包含標頭檔案的路徑比較特殊: 從當前標頭檔案所在目錄之後的目錄來搜尋標頭檔案.
比如: 標頭檔案的搜尋路徑一次為A,B,C,D,E. #include_next所在的當前標頭檔案位於B目錄, 那麼#include_next使得前處理器從C,D,E目錄來搜尋#include_next所指定的標頭檔案.
可參考cpp手冊進一步瞭解#include_next
6, 預定義巨集
標準C中定義了一些物件巨集, 這些巨集的名稱以"__"開頭和結尾, 並且都是大寫字元. 這些預定義巨集可以被#undef, 也可以被重定義.
下面列出一些標準C中常見的預定義物件巨集(其中也包含gcc自己定義的一些預定義巨集:
__LINE__ 當前語句所在的行號, 以10進位制整數標註.
__FILE__ 當前原始檔的檔名, 以字串常量標註.
__DATE__ 程式被編譯的日期, 以"Mmm dd yyyy"格式的字串標註.
__TIME__ 程式被編譯的時間, 以"hh:mm:ss"格式的字串標註, 該時間由asctime返回.
__STDC__ 如果當前編譯器符合ISO標準, 那麼該巨集的值為1
__STDC_VERSION__ 如果當前編譯器符合C89, 那麼它被定義為199409L, 如果符合C99, 那麼被定義為199901L.
我用gcc, 如果不指定-std=c99, 其他情況都給出__STDC_VERSION__未定義的錯誤資訊, 咋回事呢?
__STDC_HOSTED__ 如果當前系統是"本地系統(hosted)", 那麼它被定義為1. 本地系統表示當前系統擁有完整的標準C庫.
gcc定義的預定義巨集:
__OPTMIZE__ 如果編譯過程中使用了優化, 那麼該巨集被定義為1.
__OPTMIZE_SIZE__ 同上, 但僅在優化是針對程式碼大小而非速度時才被定義為1.
__VERSION__ 顯示所用gcc的版本號.
可參考"GCC the complete reference".
要想看到gcc所定義的所有預定義巨集, 可以執行: $ cpp -dM /dev/null
7, #line
#line用來修改__LINE__和__FILE__.
e.g.
printf("line: %d, file: %s", __LINE__, __FILE__);
#line 100 "haha"
printf("line: %d, file: %s", __LINE__, __FILE__);
printf("line: %d, file: %s", __LINE__, __FILE__);
顯示:
line: 34, file: 1.c
line: 100, file: haha
line: 101, file: haha
8, #pragma, _Pragma
#pragma用編譯器用來新增新的預處理功能或者顯示一些編譯資訊. #pragma的格式是各編譯器特定的, gcc的如下:
#pragma GCC name token(s)
#pragma之後有兩個部分: GCC和特定的pragma name. 下面分別介紹gcc中常用的.
(1) #pragma GCC dependency
dependency測試當前檔案(既該語句所在的程式程式碼)與指定檔案(既#pragma語句最後列出的檔案)的時間戳. 如果指定檔案比當前檔案新, 則給出警告資訊.
e.g.
在demo.c中給出這樣一句:
#pragma GCC dependency "temp-file"
然後在demo.c所在的目錄新建一個更新的檔案: $ touch temp-file, 編譯: $ gcc demo.c 會給出這樣的警告資訊: warning: current file is older than temp-file
如果當前檔案比指定的檔案新, 則不給出任何警告資訊.
還可以在在#pragma中給新增自定義的警告資訊.
e.g.
#pragma GCC dependency "temp-file" "demo.c needs to be updated!"
1.c:27:38: warning: extra tokens at end of #pragma directive
1.c:27:38: warning: current file is older than temp-file
注意: 後面新增的警告資訊要用""引用起來, 否則gcc將給出警告資訊.
(2) #pragma GCC poison token(s)
若原始碼中出現了#pragma中給出的token(s), 則編譯時顯示警告資訊. 它一般用於在呼叫你不想使用的函式時候給出出錯資訊.
e.g.
#pragma GCC poison scanf
scanf("%d", &a);
warning: extra tokens at end of #pragma directive
error: attempt to use poisoned "scanf"
注意, 如果呼叫了poison中給出的標記, 那麼編譯器會給出的是出錯資訊. 關於第一條警告, 我還不知道怎麼避免, 用""將token(s)引用起來也不行.
(3) #pragma GCC system_header
從#pragma GCC system_header直到檔案結束之間的程式碼會被編譯器視為系統標頭檔案之中的程式碼. 系統標頭檔案中的程式碼往往不能完全遵循C標準, 所以標頭檔案之中的警告資訊往往不顯示. (除非用 #warning顯式指明).
(這條#pragma語句還沒發現用什麼大的用處)
由於#pragma不能用於巨集擴充套件, 所以gcc還提供了_Pragma:
e.g.
#define PRAGMA_DEP #pragma GCC dependency "temp-file"
由於預處理之進行一次巨集擴充套件, 採用上面的方法會在編譯時引發錯誤, 要將#pragma語句定義成一個巨集擴充套件, 應該使用下面的_Pragma語句:
#define PRAGMA_DEP _Pragma("GCC dependency "temp-file"")
注意, ()中包含的""引用之前引該加上轉義字元.
9, #, ##
#和##用於對字串的預處理操作, 所以他們也經常用於printf, puts之類的字串顯示函式中.
#用於在巨集擴充套件之後將tokens轉換為以tokens為內容的字串常量.
e.g.
#define TEST(a,b) printf( #a "<" #b "=%d", (a)<(b));
注意: #只針對緊隨其後的token有效!
##用於將它前後的兩個token組合在一起轉換成以這兩個token為內容的字串常量. 注意##前後必須要有token.
e.g.
#define TYPE(type, n) type n
之後呼叫:
TYPE(int, a) = 1;
TYPE(long, b) = 1999;
將被替換為:
int a = 1;
long b = 1999;
(10) #warning, #error
#warning, #error分別用於在編譯時顯示警告和錯誤資訊, 格式如下:
#warning tokens
#error tokens
e.g.
#warning "some warning"
注意, #error和#warning後的token要用""引用起來!
(在gcc中, 如果給出了warning, 編譯繼續進行, 但若給出了error, 則編譯停止. 若在命令列中指定了 -Werror, 即使只有警告資訊, 也不編譯.