VC中預處理指令與巨集定義的妙用
一個經典的例子
使用預處理與巨集定義最經典的例子莫過於加在一個頭檔案中以避免標頭檔案被兩次編譯。試想這種的情況,有一個檔案headerfile.h 它被包含在headerfile1.h中,同時在headerfile2.h 中也被包含了,現在有一個CPP檔案,implement.cpp 包含了headerfile1.h 和headerfile2.h:
#include “headerfile1.h” #include “headerfile2.h” |
假設headerfile.h 中定義了一個全域性變數 iglobal 。
int iglobal; |
在編譯的時候編譯器兩次編譯headerfile,也就會發現iglobal被定義了兩次,這時就會發生變數重定義的編譯錯誤。
傳統的解決辦法是使用#ifdef 以及#endif 來避免標頭檔案的重複編譯,在上面的例子中,只需要加上這麼幾行:
#ifndef smartnose_2002_6_21_headerfile_h #define smartnose_2002_6_21_headerfile_h int iglobal; #endif |
仔細的考慮上面的巨集定義,會發現當編譯器編譯過一次headerfile.h以後,smartnose_2002_6_21_headerfile_h 這個巨集就被定義了,以後對headerfile.h的編譯都會跳過int iglobal 這一行。當然smartnose_2002_6_21_headerfile_h 這個巨集是可以任意定義的,但是這個巨集本身不能和其它檔案中定義的巨集重複,所以MFC在自動生成的檔案中總是使用一個隨機產生的長度非常長的巨集,但我覺得這沒有必要,我建議在這個巨集中加入一些有意義的資訊,比方作者,檔名,檔案建立時間等等,因為我們有時候會忘記在註釋中加入這些資訊。
在VC.Net 中我們不會再看見這些巨集定義了,因為在這裡會普遍使用一個預處理指令:
#pragma once |
只要在標頭檔案的最開始加入這條指令就能夠保證標頭檔案被編譯一次,這條指令實際上在VC6中就已經有了,但是考慮到相容性並沒有太多的使用它。
原始碼版本控制
當我們為許多平臺開發多個版本的時候預編譯指令和巨集定義也能夠幫我們的忙。假設我們現在為WINDOWS 和LINUX開發了一套軟體,由於這兩種系統的不同,我們不得不在程式控制原始碼的版本。比方記憶體的分配,我們可以在LINUX上使用標準C的malloc 函式,但是我們希望在 WINDOWS上使用HeapAlloc API。下面的程式碼演示了這種情況:
main() { ……………….. #ifdef _WINDOWS_PLATFORM HeapAlloc(5); #else malloc(5); #endif ……………….. } |
當我們在WINDOWS 平臺上編譯此程式的時候,只需要定義_WINDOWS_PLATFORM這個巨集,那麼HeapAlloc這條語句就能夠起作用了。這樣就能夠讓我們在同一個檔案中為不同的平臺實現不同版本的程式碼,同時保持程式的良好結構。在許多情況下,我們還可以為一個方法使用不同的演算法,然後用巨集定義來針對不同的情況選擇其中的一個進行編譯。這在MFC應用程式中是使用得最多的。最明顯的就是檔案中經常存在的
#ifdef _DEBUG …………………….some code……….. #endif |
這樣的程式碼,這些程式碼在應用程式的除錯版(DEBUG)中會發揮其作用。
#Pragma 指令
在所有的預處理指令中,#Pragma 指令可能是最複雜的了,它的作用是設定編譯器的狀態或者是指示編譯器完成一些特定的動作。其格式一般為
#Pragma Para
其中Para 為引數,下面來看一些常用的引數。
message 引數。 Message 引數是我最喜歡的一個引數,它能夠在編譯資訊輸出視窗中輸出相應的資訊,這對於原始碼資訊的控制是非常重要的。其使用方法為:
#Pragma message(“訊息文字”)
當編譯器遇到這條指令時就在編譯輸出視窗中將訊息文字打印出來。
當我們在程式中定義了許多巨集來控制原始碼版本的時候,我們自己有可能都會忘記有沒有正確的設定這些巨集,此時我們可以用這條指令在編譯的時候就進行檢查。假設我們希望判斷自己有沒有在原始碼的什麼地方定義了_X86這個巨集可以用下面的方法
#ifdef _X86 #Pragma message(“_X86 macro activated!”) #endif |
當我們定義了_X86這個巨集以後,應用程式在編譯時就會在編譯輸出窗口裡顯示“_X86 macro activated!”。我們就不會因為不記得自己定義的一些特定的巨集而抓耳撓腮了。
另一個使用得比較多的pragma引數是code_seg。格式如:
#pragma code_seg( ["section-name"[,"section-class"] ] ) |
它能夠設定程式中函式程式碼存放的程式碼段,當我們開發驅動程式的時候就會使用到它。
最後一個比較常用的就是上面所說的#pragma once 指令了。
VC預定義的巨集
在VC中有一類巨集並不是由使用者用#define語句定義的,而是編譯器本身就能夠識別它們。這些巨集的作用也是相當大的。讓我們來看第一個,也是MFC中使用得最頻繁的一個:__FILE__ 。
當編譯器遇到這個巨集時就把它展開成當前被編譯檔案的檔名。好了,我們馬上就可以想到可以用它來做什麼,當應用程式發生錯誤時,我們可以報告這個錯誤發生的程式程式碼在哪個檔案裡,比方在檔案test.cpp中有這樣的程式碼:
try { char * p=new(char[10]); } catch(CException *e ) { TRACE(“ there is an error in file: %s/n”,__FILE__); } |
在程式執行的時候,如果記憶體分配出現了錯誤,那麼在除錯視窗中會出現there is an error in file: test.cpp 這句話,當然,我們還可以把這個錯誤資訊顯示在別的地方。
如果我們還能夠記錄錯誤發生在哪一行就好了,幸運的是,與__FILE__巨集定義一樣,還有一個巨集記錄了當前程式碼所在的行數,這個巨集是__LINE__。使用上面的兩個巨集,我們可以寫出一個類似於VC提供的ASSERT語句。下面是方法
#define MyAssert(x) / if(!(x)) / MessageBox(__FILE__,__LINE__,NULL,MB_OK); |
我們在應用程式中可以象使用ASSERT語句一樣使用它,在錯誤發生時,它會彈出一個對話方塊,其標題和內容告訴了我們錯誤發生的檔案和程式碼行號,方便我們的除錯,這對於不能使用ASSERT語句的專案來說是非常有用的。
除了這兩個巨集以外,還有記錄編譯時間的__TIME__,記錄日期的__DATE__,以及記錄檔案修改時間的__TIMESTAMP__巨集。
使用這些預定義的巨集,我們幾乎可以生成和VC能夠生成的一樣完整的原始碼資訊報表。
結論
翻開MFC和Linux的原始碼,巨集定義幾乎佔據了半邊天,訊息對映,佇列操作,平臺移植,版本管理,甚至核心模組的拆卸安裝都用巨集定義完成。毫不誇張的說,有些檔案甚至就只能看見巨集定義。所以學習巨集定義,熟練的使用巨集定義對於學習C語言乃至VC都是非常關鍵的。
在上文中,我演示了幾個常用的巨集定義和預處理指令,但可以說這些都是相當常規的技巧。下面要介紹的巨集定義與預處理指令的用法也是ATL,MFC以及LINUX中使用得比較多的非常重要的技巧。
## 連線符與# 符
## 連線符號由兩個井號組成,其功能是在帶引數的巨集定義中將兩個子串(token)聯接起來,從而形成一個新的子串。但它不可以是第一個或者最後一個子串。所謂的子串(token)就是指編譯器能夠識別的最小語法單元。具體的定義在編譯原理裡有詳盡的解釋,但不知道也無所謂。同時值得注意的是#符是把傳遞過來的引數當成字串進行替代。下面來看看它們是怎樣工作的。這是MSDN上的一個例子。
假設程式中已經定義了這樣一個帶引數的巨集:
#define paster( n ) printf( "token" #n " = %d", token##n )
同時又定義了一個整形變數:
int token9 = 9;
現在在主程式中以下面的方式呼叫這個巨集:
paster( 9 );
那麼在編譯時,上面的這句話被擴充套件為:
printf( "token" "9" " = %d", token9 );
注意到在這個例子中,paster(9);中的這個”9”被原封不動的當成了一個字串,與”token”連線在了一起,從而成為了token9。而#n也被”9”所替代。
可想而知,上面程式執行的結果就是在螢幕上列印出token9=9
在ATL的程式設計中,我們檢視它的原始碼就會經常看見這樣的一段:
#define IMPLEMENTS_INTERFACE(Itf) /
{&IID_##Itf, ENTRY_IS_OFFSET,BASE_OFFSET(_ITCls, Itf) },
我們經常不假思索的這樣使用它:
……
IMPLEMENTS_INTERFACE(ICat)
……
實際上IID_ICat 已經在別的地方由ATL嚮導定義了。當沒有嚮導的時候,你只要遵循把IID_加在你的介面名前面來定義GUID的規則就也可以使用這個巨集。在實際的開發過程中可能很少用到這種技巧,但是ATL使用得如此廣泛,而其中又出現了不少這樣的原始碼,所以明白它是怎麼一回事也是相當重要的。我的一個朋友就是因為不知道IMPLEMENTS_INTERFACE巨集是怎麼定義的,而又不小心改動了IID_ICat的定義而忙活了一整天。
Linux的怪圈
在剛開始閱讀Linux的時候有一個小小的巨集讓我百思不得其解:
#define wait_event(wq,condition) /
do{ /
if(condition) /
break; /
__wait_event(wq,condition); /
}while(0)
這是一個奇怪的迴圈,它根本就只會執行一次,為什麼不去掉外面的do{..}while結構呢?我曾一度在心裡把它叫做“怪圈”。原來這也是非常巧妙的技巧。在工程中可能經常會引起麻煩,而上面的定義能夠保證這些麻煩不會出現。下面是解釋:
假設有這樣一個巨集定義
#define macro(condition) /
if(condition) dosomething();
現在在程式中這樣使用這個巨集:
if(temp)
macro(i);
else
doanotherthing();
一切看起來很正常,但是仔細想想。這個巨集會展開成:
if(temp)
if(condition) dosomething();
else
doanotherthing();
這時的else不是與第一個if語句匹配,而是錯誤的與第二個if語句進行了匹配,編譯通過了,但是執行的結果一定是錯誤的。
為了避免這個錯誤,我們使用do{….}while(0) 把它包裹起來,成為一個獨立的語法單元,從而不會與上下文發生混淆。同時因為絕大多數的編譯器都能夠識別do{…}while(0)這種無用的迴圈並進行優化,所以使用這種方法也不會導致程式的效能降低。
幾個小小的警告
正如微軟聲稱的一樣,巨集定義與預編譯器指令是強大的,但是它又使得程式難以除錯。所以在定義巨集的時候不要節省你的字串,一定要力爭完整的描述這個巨集的功能。同時在定義巨集的時候如有必要(比方使用了if語句)就要使用do{…}while(0)將它封閉起來。在巨集定義的時候一定要注意各個巨集之間的相互依賴關係,儘量避免這種依賴關係的存在。下面就有這樣一個例子。
設有一個靜態陣列組成的整型佇列,在定義中使用了這樣的方法: int array[]={5, 6, 7, 8};
我們還需要在程式中遍歷這個陣列。通常的做法是使用一個巨集定義
#define ELE_NUM 4
…………………………..
……………………………..
for(int I=0;I<ELE_NUM;I++)
{
cout<<array[I];
}
由於某種偶然的原因,我們刪除了定義中的一個元素,使它變成:
array[]={5,6,7}
而卻忘了修改ELE_NUM的值。那麼在上面的程式碼中馬上就會發生訪問異常,程式崩潰。然後是徹夜不眠的除錯,最後發現問題出在這個巨集定義上。解決這個問題的方法是不使用
array[]={….}這樣的定義,而顯式的申明陣列的大小:
array[ELE_NUM]={….}
這樣在改動陣列定義的時候,我們就不會不記得去改巨集定義了。總之,就是在使用巨集定義的時候能夠用巨集定義的地方統統都用上。
我發現的另一個有趣的現象是這樣的:
假設現在有一個課程管理系統,學生的人數用巨集定義為:
#define STU_NUM 50
而老師的人數恰好也是50人,於是很多人把所有涉及到老師人數的地方通通用上STU_NUM這個巨集。另一個學期過去,學生中的一個被開除了,系統需要改變。怎麼辦呢?簡單的使用#define STU_NUM 49 麼?如果是這樣,一個老師也就被開除了,我們不得不手工在程式中去找那些STU_NUM巨集然後判斷它是否是表示學生的數目,如果是,就把它改成49。天哪,這個巨集定義製造的麻煩比使用它帶來的方便還多。正確的方法應該是為老師的數目另外定義一個巨集:
#define TEA_NUM 50
當學生的數目改變以後只要把STU_NUM 定義為49就完成了系統的更改。所以,當程式中的兩個量之間沒有必然聯絡的時候一定不要用其中的一個巨集去替代另一個,那隻會讓你的程式根本無法改動。
最後,建議C/C++語言的初學者儘可能多的在你的程式中使用巨集定義和預編譯指令。多看看MFC,ATL或者LINUX的原始碼,你會發現C語言強大的原因所在。