C/C++中幾個宏的簡單總結
作者:magictong
環境:VS2005 XPSP3
有人視宏為洪水猛獸,甚至要求完全從C/C++中摒棄,有人則認為宏為至尊寶典,在邏輯代碼中都大量使用。個人認為這是個仁者見仁智者見智的問題,摒棄就沒必要了,看看宏在MFC和ATL中的一些經典應用,你會發現如果不使用宏來實現一些訊息映射和對象映射神馬的那將讓“苦逼”程式員多花費多少寶貴的時間。當然也不能濫用,尤其是盡量不要在邏輯代碼中使用,宏中的邏輯出問題後,調試時候的痛苦你就真的會發現原來程式員真的挺“苦逼”的。
其實很多人對宏的“恐懼”可能源於下面的一個簡單的宏的實現:
#define SUM(x, y) x+y
看起來是正確的,但是當你這樣使用的時候SUM(3, 4) * SUM(2, 5),你會發現結果並不是想象的49,而是詭異的16。其實宏僅僅完成簡單的替換,而不會像函數那樣進行參數計算並且調用返回,上面那個式子SUM(3, 4) * SUM(2, 5)實際會被替換為:3+4*2+5,現在知道為什麼結果是16了吧,那我們實現這樣應該咯:
#define SUM(x, y) (x+y)
把整個用小括弧包起來,看似好像正確了,但是如果這樣調用SUM(3||4, 4),結果會是5嗎?呃,不是,結果是1,哪裡出了問題?我們看看展開的結果3||4+4,哦,因為加法的運算層級更高3||4+4 = 3||8 = 1,看來我們得把參數也括起來:
#define SUM(x, y) ((x)+(y))
經曆了上面的一些跌跌撞撞的失敗和嘗試後我們終於得出了一個正確的兩個數求和的正確版本。
從上面的那個例子我想大家應該會受到一些啟發以及可以看到寫一個宏要注意的一些要點。不過我們今天要討論的話題則是另外幾個可能用得不多但是比較有用的宏。
1、 怎樣通過一個簡單的宏得到一個field在結構體(struct)中的位移量 ?
你可以進行如下的定義:
#define FieldOffset(type, field) ((unsigned int) &((type *)0)->field)
解釋:將0強制轉換為type類型指標,然後訪問field域,注意不是真的訪問,而是對它求地址,將該地址減去結構的基地址0就是位移量了,因為基地址是0省略。
同樣得到一個結構體中field所佔用的位元組數可以定義為:
#define FieldSize(type, field) sizeof(((type *) 0)->field)
2、 #、##和#@的用法
#把一個宏參數變成字串(也就是給參數加上雙引號)
##用來把宏參數簡單串連在一起
#@把一個宏參數變成字元(也就是給參數加上單引號)
#define TOSTRING(s) #s
#define TOCHAR(s) #@s
#define TOKENCONN(s, t) s##t
另外,對於#@如果你這樣調用TOCHAR(abcd),會返回’d’,但是如果TOCHAR(abcde),則會編譯出錯,看來這與VS的宏處理器有關係,雖然這麼使用很詭異。
更重要的一點如果宏定義裡用到#、#@或##的地方宏參數可能不會像想象中展開,怎麼理解?一般來講如果宏參數裡面有另外的宏,會進行遞迴的替換:
#define C 7
#define B C
#define A B
#define SUM(x, y) ((x)+(y))
然後這樣調用int x = SUM(A, A);編譯沒有問題,最後x的值是14,替換過程如下:
SUM(A, A) => ((A)+(A)) => ((B)+(B)) => ((C)+(C)) => ((7)+(7))
但是如果有這樣的定義:
#define D 12
#define TOSTRING(s) #s
#define TOKENCONN(s, t) s##t
則,TOSTRING(D)的結果是”D”,而TOKENCONN(D, D)的結果則是DD,如果沒有定義DD標識符則會編譯報錯。原因在於,宏替換是分層次的,先替換最外層的,然後再替換參數,而這三個特殊符號都有改變宏參數的含義的作用(把宏參數改變為字元,字串或者不同的token),因此造成不會有多級展開。
這樣會帶來一個問題,就是我需要用到這3個特殊的標識符(#、#@或##),而宏參數我又需要展開,該怎麼辦,我之前在KM上看到過這樣的一個需求,我們先看一個我常用的宏:
#pragma message("messageinfo: no impl, TODO…")
我經常使用這個宏插在代碼中來標記我沒有實現的功能或者需要後面改進的地方,這個宏的好處是在編譯的時候,會在編譯資訊輸出視窗中顯示出你寫在message裡面的資訊。防止你寫代碼到後面的時候忘記哪些地方還沒有實現。但是我使用的方法很原始,如果需要實現某個功能的時候,我就去全域搜尋那個message裡面的資訊,然後再找到地方,後來在KM上面看到一種類似的需求,可以利用IDE本身提供的功能直接定位到寫這段宏的地方,如果編譯報錯的時候,你會看到類似下面的輸出:
1>f:\e\projects\tu_func\tu_func.cpp(124) : error C2065: 'DD' : undeclared identifier
此時如果你雙擊這一行,代碼視窗就會跳轉到編譯出錯的地方去,我們要做的就是類比這樣的一個輸出,利用兩個已經定義過的宏__FILE__和__LINE__,這兩個宏神馬作用我就不講了,我的第一個類比版本如下:
#define TOSTRING(s) #s
#define MagicTipsMsg(msg) message(__FILE__"(" TOSTRING (__LINE__)"):"#msg)
結果很悲催,沒達到預期:
1>f:\e \projects\tu_func\tu_func.cpp(__LINE__):there is need to implement
很顯然出現了上面說到的宏參數沒展開的問題,腫麼辦?其實解決這個問題的方法也不是很複雜,在中間加一層跳板宏就可以了,加這個跳板宏的用意是先把所有宏的參數在這個跳板宏這層裡全部展開, 那麼最終的字串轉換宏那裡(TOSTRING)就可以得到正確的宏參數了。
#define TOSTRING(s) #s
#define __TOSTRING(s) TOSTRING (s)
#define MagicTipsMsg(msg) message(__FILE__"(" __TOSTRING(__LINE__)"):"#msg)
現在在編譯視窗的輸出終於正確了:
1>f:\e\projects\tu_func\tu_func.cpp(121):there is need to implement
3、 幾個預定義的宏
__LINE__
__FILE__
__DATE__
__TIME__
__cplusplu
是否全部支援上面的幾個宏,與編譯器的具體實現有關。__LINE__是表示宏所在的當前的檔案的具體行數,__FILE__是表示當前檔案的全路徑名,__DATE__巨集指令含有形式為月/日/年的串,表示源檔案被編譯時間的日期。而原始碼編譯為目標代碼的時間作為串包含在__TIME__中。具體表現形式大家自己試一試。
在編譯C++程式時,編譯器自動定義了一個預先處理名 __cplusplus,要想知道是否define了這某個宏怎麼辦?可以做一個類似如下的檢查:
#ifdef __cplusplus
#pragma message("__cplusplus definded")
#endif
4、 用#pragma匯出dll中的函數
一般我們用的比較多的匯出dll中函數的方法是使用模組定義檔案(.def),而實際上VC提供了一個擴充的方法,使用__declspec(dllexport)去匯出某個函數:
int __declspec(dllexport) add(int a, int b)
此時匯出的函數名為“?add@@YAHHH@Z”如果我們希望進行dll動態連結,上面的匯出函數的名稱就有點太坑爹了,而且一旦對函數的傳回值或者參數類型進行了修改,函數名也要跟著修改,我們希望匯出函數名是“add”。至少有兩種方法,一是使用def檔案來匯出,二是在函式宣告的最前面加上extern “C”:
extern “C” int __declspec(dllexport) add(int a, int b)
不過,我們今天要是要討論VC 提供的另一個預先處理宏來解決這個問題,如下:
#pragma comment(linker,"/EXPORT:add=?add@@YAHHH@Z ")
這時,實際上會匯出?add@@YAHHH@Z和add兩個函數,但是函數的進入點是一樣的。實際上這個宏的強大不僅僅如此,它的格式如下(MSDN:http://msdn.microsoft.com/en-us/library/hyx1zcd3(v=vs.80).aspx):
/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]
說明:@ordinal用來指定順序,NONAME指定只將函數匯出為序號,DATA 關鍵字指定匯出項為資料項目。例如:
#pragma comment(linker,"/EXPORT:add=?add@@YAHHH@Z,@2,NONAME")
就是把add函數無名匯出並且設定順序為2。
因此如果你想指定匯出的順序,或者只將函數匯出為序號,沒有函數名,這都是能夠實現的,之前和同事討論到怎麼把一個函數無名匯出(很多系統dll的匯出表裡面就沒有匯出函數的名字),原來通過這個預先處理宏就可以簡單解決。
另一個問題是無名匯出的函數自己怎麼動態連結呢(使用如下的方法,假設匯出順序為2,這種無名匯出除了增加神秘感還有神馬用途暫時還沒想到):
typedef int (*PFUNADD)(int, int);
HINSTANCE hIns = LoadLibrary(TEXT("dllname.dll"));
PFUNADD pfnAdd = (PFUNADD)GetProcAddress(hIns, MAKEINTRESOURCE(2));
int c = pfnAdd(1, 2);