C中的預編譯宏定義

C中的預編譯宏定義

2009-02-10 作者: infobillows 來源:網絡

在將一個C源程序轉換為可執行程序的過程中, 編譯預處理是最初的步驟. 這一步驟是由預處理器(preprocessor)來完成的. 在源流程序被編譯器處理之前, 預處理器首先對源程序中的"宏(macro)"進行處理.

C初學者可能對預處理器沒什么概念, 這是情有可原的: 一般的C編譯器都將預處理, 匯編, 編譯, 連接過程集成到一起了. 編譯預處理往往在后臺運行. 在有的C編譯器中, 這些過程統統由一個單獨的程序來完成, 編譯的不同階段實現這些不同的功能. 可以指定相應的命令選項來執行這些功能. 有的C編譯器使用分別的程序來完成這些步驟. 可單獨調用這些程序來完成. 在gcc中, 進行編譯預處理的程序被稱為CPP, 它的可執行文件名為cpp.

編譯預處理命令的語法與C語言的語法是完全獨立的. 比如: 你可以將一個宏擴展為與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 insert(stmt) stmt

insert ( a=1; b=2;)? 相當于在代碼中加入 a=1; b=2 .

insert ( a=1, b=2;)? 就有問題了: 預處理器會提示出錯: 函數宏的參數個數不匹配. 預處理器把","視為參數間的分隔符.

insert ((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\n", __LINE__, __FILE__);

#line 100 "haha"

printf("line: %d, file: %s\n", __LINE__, __FILE__);

printf("line: %d, file: %s\n", __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\n", (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, 即使只有警告信息, 也不編譯.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容