C/C++中宏總結C程式的原始碼中可包括各種編譯指令,這些指令稱為預先處理命令。雖然它們實際上不是C語言的一部分,但卻擴充了C程 序設計的環境。本節將介紹如何應用預先處理程式和注釋簡化程式開發過程,並提高程式的可讀性。
宏不是C/C++ 語言的一部分,宏在編譯前由前置處理器處理。 宏的部分功能在C++ 中可以由template、inline、const代替。我認為現在保留在C++中的最大用處是自動代碼產生
ANSI標準定義的C語言預先處理程式包括下列命令:
#define,#error,#i nclude,#if,#else,#elif,#endif,#ifdef,#ifndef,#undef,#line,#pragma等。非常明顯,所有預先處理命令均以符號#開頭,下面分別加以介紹。
1、#define
命令#define定義了一個標識符及一個串。在來源程式中每次遇到該標識符時,均以定義的串代換它。ANSI標準將標識符定義為宏名,將替換過程稱為宏 替換。命令的一般形式為:
#define identifier string
注意:
該語句沒有分號。在標識符和串之間可以有任意個空格,串一旦開始,僅由一新行結束。
宏名定義後,即可成為其它宏名定義中的一部分。
宏替換僅僅是以文本串代替宏標識符,前提是宏標識符必須獨立的識別出來,否則不進行替換。例如:
#define XYZ this is a test,使用宏printf("XYZ");//該段不列印"this is a test"而列印"XYZ"。因為先行編譯器識 別出的是"XYZ"
如果串長於一行,可以在該行末尾用一反斜線' \'續行。
2、#error
處理器命令#error強迫編譯器停止編譯,主要用於程式調試。
3、#i nclude
命令#i nclude使編譯器將另一源檔案嵌入帶有#i nclude的源檔案,被讀入的源檔案必須用雙引號或角括弧括起來。例如:
#include"stdio.h"或者#include
這兩行代碼均使用C編譯器讀入並編譯用於處理磁碟檔案庫的子程式。
將檔案嵌入#include命令中的檔案內是可行的,這種方式稱為嵌套的嵌入檔案,嵌套層次依賴於具體實現。
如果顯式路徑名為檔案標識符的一部分,則僅在哪些子目錄中搜尋被嵌入檔案。否則,如果檔案名稱用雙引號括起來,則首先檢索當前工作目錄。如果未發現檔案, 則在命令列中說明的所有目錄中搜尋。如果仍未發現檔案,則搜尋實現時定義的標準目錄。
如果沒有顯式路徑名且檔案名稱被角括弧括起來,則首先在編譯命令列中的目錄內檢索。
如果檔案沒找到,則檢索標準目錄,不檢索當前工作目錄。
4、條件編譯命令
有幾個命令可對程式原始碼的各部分有選擇地進行編譯,該過程稱為條件編譯。商業軟體公司廣泛應用條件編譯來提供和維護某一程式的許多顧客版本。
#if、#else,#elif及#endif
#if的一般含義是如果#if後面的常量運算式為true,則編譯它與#endif之間的代碼,否則跳過這些代碼。命令#endif標識一個#if塊的 結束。
#if constant-expression
statement sequence
#endif
跟在#if後面的運算式在編譯時間求值,因此它必須僅含常量及已定義過的標識符,不可使用變數。運算式不許含有操作符sizeof(sizeof也是編譯 時求值)。
#else命令的功能有點象C語言中的else;#else建立另一選擇(在#if失敗的情況下)。
注意,# else屬於# if塊。
#elif命令意義與ELSE IF 相同,它形成一個if else-if階梯狀語句,可進行多種編譯選擇。
#elif 後跟一個常量運算式。如果運算式為true,則編譯其後的代碼塊,不對其它#elif運算式進行測試。否則,已排序的測試下一塊。
#if expression
statement sequence
#elif expression1
statement sequence
#endif
在嵌套的條件編譯中#endif、#else或#elif與最近#if或#elif匹配。
# ifdef 和# ifndef
條件編譯的另一種方法是用#ifdef與#ifndef命令,它們分別表示"如果有定義"及"如果無定義"。
# ifdef的一般形式是:
# ifdef macroname
statement sequence
#endif
#ifdef與#ifndef可以用於#if、#else,#elif語句中,但必須與一個#endif。
5、#undef
命令#undef 取消其後那個前面已定義過有宏名定義。一般形式為:
#undef macroname
6、#line
命令# line改變__LINE__與__FILE__的內容,它們是在編譯器中預先定義的標識符。命令的基本形式如下:
# line number["filename"]
其中的數字為任何正整數,可選的檔案名稱為任意有效檔案標識符。行號為來源程式中當前行號,檔案名稱為源檔案的名字。命令# line主要用於調試及其它特殊 應用。
注意:在#line後面的數位識別碼從下一行開始的數位識別碼。
7、預定義的宏名
ANSI標準說明了C中的五個預定義的宏名。它們是:
__LINE__
__FILE__
__DATE__
__TIME__
__STDC__
如果編譯不是標準的,則可能僅支援以上宏名中的幾個,或根本不支援。記住編譯器也許還提供其它預定義的宏名。
__LINE__及__FILE__巨集指令在有關# line的部分中已討論,這裡討論其餘的宏名。
__DATE__巨集指令含有形式為月/日/年的串,表示源檔案被翻譯到代碼時的日期。
原始碼翻譯到目標代碼的時間作為串包含在__TIME__中。串形式為時:分:秒。
如果實現是標準的,則宏__STDC__含有十進位常量1。如果它含有任何其它數,則實現是非標準的。編譯C++程式時,編譯器自動定義了一個預先處理名 字__cplusplus,而編譯標準C時,自動定義名字__STDC__。
注意:宏名的書寫由標識符與兩邊各二條底線構成。
(部分內容出自:http://www.bc-cn.net/Article/kfyy/cyy/jc/200511/919.html)
8、C、C++宏體中出現的#,#@,##
宏體中,#的功能是將其後面的宏參數進行字串化操作(Stringfication),簡單說就是在對它所引用的宏變數通過替換後在其左右各加上一個 雙引號。
而##被稱為串連符(concatenator),用來將兩個Token串連為一個Token。注意這裡串連的對象是Token就行,而不一定是宏的變 量。比如你要做一個功能表項目命令名和函數指標組成的結構體的數組,並且希望在函數名和功能表項目命令名之間有直觀的、名字上的關係。那就可以使用:宏參數## 固定部分。當然還可以n個##符號串連 n+1個Token,這個特性也是#符號所不具備的。
#@的功能是將其後面的宏參數進行字元化。
9、C宏中的變參...
...在C宏中稱為Variadic Macro,也就是變參宏。比如:
#define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__)
或者#define myprintf(templt,args...) fprintf(stderr,templt,args)
第一個宏中由於沒有對變參起名,我們用預設的宏__VA_ARGS__來替代它。第二個宏中,我們顯式地命名變參為args,那麼我們在宏定義中就可以 用args來代指變參了。同C語言的stdcall一樣,變參必須作為參數表的最後有一項出現。當上面的宏中我們只能提供第一個參數templt時,C 標準要求我們必須寫成: myprintf(templt,);的形式。這時的替換過程為:myprintf("Error!\n",);替換為: fprintf(stderr,"Error!\n",).
這是一個語法錯誤,不能正常編譯。這個問題一般有兩個解決方案。首先,GNU CPP提供的解決方案允許上面的宏調用寫成: myprintf(templt);而它將會被通過替換變成: fprintf(stderr,"Error!\n",);
很明顯,這裡仍然會產生編譯錯誤(非本例的某些情況下不會產生編譯錯誤)。除了這種方式外,c99和GNU CPP都支援下面的宏定義方式:
#define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)
這時,##這個串連符號充當的作用就是當__VAR_ARGS__為空白的時候,消除前面的那個逗號。那麼此時的翻譯過程如下: myprintf(templt);被轉化為: fprintf(stderr,templt);
這樣如果templt合法,將不會產生編譯錯誤。
10、#pragma的使用【轉載】
在所有的預先處理指令中,#Pragma 指令可能是最複雜的了,它的作用是設定編譯器的狀態或者是指示編譯器完成一些特定的動作。#pragma指令對 每個編譯器給出了一個方法,在保持與C和C ++語言完全相容的情況下,給出主機或作業系統專有的特徵。依據定義,編譯指示是機器或作業系統專有的,且 對於每個編譯器都是不同的。
其格式一般為: #Pragma Para,其中Para 為參數,下面來看一些常用的參數。
(1)message 參數。 Message 參數是我最喜歡的一個參數,它能夠在編譯資訊輸出視窗中輸出相應的資訊,這對於原始碼資訊的控制是非常 重要的。其使用方法為:
#Pragma message("訊息文本")
當編譯器遇到這條指令時就在編譯輸出視窗中將訊息文本列印出來。
當我們在程式中定義了許多宏來控制原始碼版本的時候,我們自己有可能都會忘記有沒有正確的設定這些宏,此時我們可以用這條指令在編譯的時候就進行檢查。 假設我們希望判斷自己有沒有在原始碼的什麼地方定義了_X86這個宏可以用下面的方法
#ifdef _X86
#Pragma message("_X86 macro activated!")
#endif
當我們定義了_X86這個宏以後,應用程式在編譯時間就會在編譯輸出視窗裡顯示"_
X86 macro activated!"。我們就不會因為不記得自己定義的一些特定的宏而抓耳撓腮了。
(2)另一個使用得比較多的pragma參數是code_seg。格式如:
#pragma code_seg( ["section-name"[,"section-class"] ] )
它能夠設定程式中函數代碼存放的程式碼片段,當我們開發驅動程式的時候就會使用到它。
(3)#pragma once (比較常用)
只要在標頭檔的最開始加入這條指令就能夠保證標頭檔被編譯一次,這條指令實際上在VC6中就已經有了,但是考慮到相容性並沒有太多的使用它。
(4)#pragma hdrstop表示先行編譯標頭檔到此為止,後面的標頭檔不進行先行編譯。BCB可以先行編譯標頭檔以加快連結的速度,但如果所有頭文 件都進行先行編譯又可能占太多磁碟空間,所以使用這個選項排除一些標頭檔。
有時單元之間有依賴關係,比如單元A依賴單元B,所以單元B要先於單元A編譯。你可以用#pragma startup指定編譯優先順序,如果使用了 #pragma package(smart_init) ,BCB就會根據優先順序的大小先後編譯。
(5)#pragma resource "*.dfm"表示把*.dfm檔案中的資源加入工程。*.dfm中包括表單、外觀的定義。
(6)#pragma warning( disable : 4507 34; once : 4385; error : 164 )
等價於:
#pragma warning(disable:4507 34) // 不顯示4507和34號警告資訊
#pragma warning(once:4385) // 4385號警告資訊僅報告一次
#pragma warning(error:164) // 把164號警告資訊作為一個錯誤。
同時這個pragma warning 也支援如下格式:
#pragma warning( push [ ,n ] )
#pragma warning( pop )
這裡n代表一個警告層級(1---4)。
#pragma warning( push )儲存所有警告資訊的現有的警告狀態。
#pragma warning( push, n)儲存所有警告資訊的現有的警告狀態,並且把全域警告層級設定為n。
#pragma warning( pop )向棧中彈出最後一個警告資訊,在入棧和出棧之間所作的一切改動取消。例如:
#pragma warning( push )
#pragma warning( disable : 4705 )
#pragma warning( disable : 4706 )
#pragma warning( disable : 4707 )
//.......
#pragma warning( pop )
在這段代碼的最後,重新儲存所有的警告資訊(包括4705,4706和4707)。 (7)pragma comment(...)
該指令將一個注釋記錄放入一個對象檔案或可執行檔中。
常用的lib關鍵字,可以幫我們連入一個庫檔案。
(8)用pragma匯出dll中的函數
傳統的到出 DLL 函數的方法是使用模組定義檔案 (.def),Visual C++ 提供了更簡潔方便的方法,那就 是"__declspec()"關鍵字後面跟"dllexport",告訴串連去要匯出這個函數,例如:
__declspec(dllexport) int __stdcall MyExportFunction(int iTest);
把"__declspec(dllexport)"放在函式宣告的最前面,串連產生的 DLL 就會匯出函 數"_MyExportFunction@4"。
上面的匯出函數的名稱也許不是我的希望的,我們希望匯出的是原版的"MyExportFunction"。還好,VC 提供了一個預先處理指示 符"#pragma"來指定串連選項 (不僅僅是這一個功能,還有很多指示功能) ,如下:
#pragma comment(linker,"/EXPORT:MyExportFunction=_MyExportFunction@4")
這下就天如人願了:)。如果你想指定匯出的順序,或者只將函數匯出為序號,沒有 Entryname,這個預先處理指示符 (確切地說是連接器) 都能夠 實現,看看 MSDN 的文法說明:
/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]
@ordinal 指定順序;NONAME 指定只將函數匯出為序號;DATA 關鍵字指定匯出項為資料項目。
⑨每個編譯器可以用#pragma指令啟用或終止該編譯器支援的一些編譯功能。例如,對迴圈最佳化功能:
#pragma loop_opt(on) // 啟用
#pragma loop_opt(off) // 終止
有時,程式中會有些函數會使編譯器發出你熟知而想忽略的警告,如"Parameter xxx is never used in function xxx",可以這樣:
#pragma warn -100 // Turn off the warning message for warning #100
int insert_record(REC *r)
{ /* function body */ }
#pragma warn +100 // Turn the warning message for warning #100 back on
函數會產生一條有唯一特徵碼100的警告資訊,如此可暫時終止該警告。
每個編譯器對#pragma的實現不同,在一個編譯器中有效在別的編譯器中幾乎無效。可從編譯器的文檔中查看。 ⑩#pragm pack()的使用
#pragma pack規定的對齊長度,實際使用的規則是:
結構,聯合,或者類的資料成員,第一個放在位移為0的地方,以後每個資料成員的對齊,按照#pragma pack指定的數值和這 個資料成員自身長度中,比較小的那個進行。
也就是說,當#pragma pack的值等於或超過所有資料成員長度的時候,這個值的大小將不產生任何效果。
而結構整體的對齊,則按照結構體中最大的資料成員 和 #pragma pack指定值之間,較小的那個進行。
注意:檔案使用#pragma pack(n) 改變了預設設定而不恢複,通常可以使用#pragma pack(push, n)和#pragma pack(pop)進行設定與恢複。
註:關於宏函數的內容在另外的專題。關於宏使用的誤區在描述宏的時候已經在文中提到了,最後再給出一個例子,描述的Side Effect是指宏在展開 的時候對其參數可能進行多次Evaluation(也就是取值)對程式造成的錯誤影響。
假設在一個系統中,有一個32b的寄存器(REG)儲存狀態,其中高16b表示一種含義,低16b表示另一種含義(這在程式中經常出現)。現在要把高低 16b分開,不考慮實際中的特殊要求,將代碼寫成:
#define High16bit(REG) (REG>>16)
#define Low16bit(REG) ((REG<<16)>>16)
對於這種寫法完成的功能在大多數情況是足夠了,這裡不討論。主要談論這種寫法的負面影響,如果在程式中分別在不同的語句中使用High16bit和 Low16bit,那麼就可能那就是Side effect,特別寄存器REG是狀態寄存器,他的狀態可能隨時變化,那麼引起的問題就是高低16b根本 取的不是同一個時刻狀態寄存器。這種錯誤在程式中找出就比較難了。在這裡我把條件弱化了,試想在一個宏體中,如果對參數多次取值也是可能引起問題,那就 更難了。