VC中預處理指令與宏定義的妙用之二
在 上一篇 文章中,我演示了幾個常用的宏定義和預處理指令,但可以說這些都是相當常規的技巧。下面要介紹的宏定義與預處理指令的用法也是ATL,MFC以及 LINUX 中使用得比較多的非常重要的技巧。 ## 連接符與# 符 ## 連接符號由兩個井號組成,其功能是在帶參數
在
上一篇文章中,我演示了幾個常用的宏定義和預處理指令,但可以說這些都是相當常規的技巧。下面要介紹的宏定義與預處理指令的用法也是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[]={….}這樣的定義,而顯式的申明數組的大?。?BR>
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語言強大的原因所在。