C/C++中幾個宏的總結

有人視宏爲洪水猛獸,甚至要求完全從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);


本文轉自:http://blog.csdn.net/magictong/article/details/6818560

發佈了27 篇原創文章 · 獲贊 10 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章