C/C++-技巧-巨集

十日十乞001發表於2017-06-13

一、巨集基礎

巨集在c/c++中扮演者比較重要的角色,雖然難以閱讀和除錯的缺點讓巨集的使用飽受詬病,但是在一些特殊的情況下,使用巨集會帶來極大的方便,甚至可以實現一些用其他方式無法實現的功能。

在c/c++程式編譯的過程中,編譯器對巨集的處理是在預編譯階段進行的,處理方式的核心思想是:簡單替換,編譯器並不會對巨集本身和巨集的引數進行任何型別、語法上的檢查,這也是導致巨集不易閱讀、不易除錯的原因,也可能產生一些比較隱蔽的陷阱破環程式原本設計的邏輯。

1、巨集的分類

巨集物件:沒有引數的巨集。這類巨集常常被用來定義常量,通常比較簡單,例如:

[cpp] view plain copy
  1. #define MAX_NUM 100  
巨集函式:帶有引數的巨集。這類巨集的應用場景很多,比如定義函式、產生程式碼等等,隨著用法的不同,難易程度也有很大的波動,例如:

[cpp] view plain copy
  1. #define MAX(a, b) ((a)>(b) ? (a) : (b))  

2、巨集的操作符

#:字串化一個巨集引數,即在引數名字前後加上"。例如:

[cpp] view plain copy
  1. #define STRINGIZE(arg) #arg  
注意:當arg中包含空格的時候,前處理器只會保留一個空格,比如STRINGIZE(abc    abc)將會被替換成"abc abc",但是arg前後的空格將被忽略;當arg中包含特殊字元時,前處理器會自動新增上轉義字元'/'以保證#arg返回完整的字串化後的arg,比如STRINGIZE("a'b/c")將返回"/"a/'b//c/"",但是前提是arg本身不會對巨集STRINGIZE語句引數影響,比如STRINGIZE(abc')將產生錯誤。

#@:字元化一個巨集引數,即在引數名字前後加上'。例如:

[cpp] view plain copy
  1. #define CHARIZE(arg) #@arg  
##:拼接巨集引數和另一個符號,即連線兩個符號生成一個新的符號。例如:

[cpp] view plain copy
  1. #define SYMBOL_CATENATE(arg1, arg2) arg1 ## arg2  

注意:如果#、##操作的引數也是一個巨集,那麼這個巨集將不會被繼續展開,但是如果確實需要#、##後的巨集繼續展開,也可以定義輔助巨集過度一下:

[cpp] view plain copy
  1. #define CHARIZE_WITH_MACRO(arg) CHARIZE(arg)  
  2. #define SYMBOL_CATENATE_WITH_MACRO(arg1, arg2) SYMBOL_CATENATE(arg1, arg2)  

\:換行,即開始新的一行繼續定義巨集體。例如:

[cpp] view plain copy
  1. #define DEFINE_VARIABLE(name1, name2, type) type name1; \  
  2.     type name2;  

3、變參巨集

巨集函式也可以接受個數不定的引數,形參寫為...,在巨集體內獲取形參使用__VA_ARGS__,例如:

[cpp] view plain copy
  1. #define PRINTF(format, ...) printf(format, __VA_ARGS__);  
注意:當__VA_ARGS__作為巨集實參再次被傳入另一個巨集函式的時候,在VC下直接編譯時__VA_ARGS__只會被解釋為一個引數,例如下面程式碼:

[cpp] view plain copy
  1. #define ATTR_1(arg) printf(arg);  
  2. #define ATTR_2(arg, ...) ATTR_1(arg) ATTR_1(__VA_ARGS__)  
  3. #define ATTR_3(arg, ...) ATTR_1(arg) ATTR_2(__VA_ARGS__)  
ATTR_3("1", "2", "3")將會產生編譯錯誤,因為檢視其巨集展開後的實際程式碼為:printf("1"); printf("2", "3"); printf();,即ATTR_2(__VA_ARGS__)將("2", "3")當成了一個引數。解決辦法是使用輔助巨集ATTR():

[cpp] view plain copy
  1. #define ATTR(args) args  
  2. #define ATTR_1(arg) printf(arg);  
  3. #define ATTR_2(arg, ...) ATTR_1(arg) ATTR(ATTR_1(__VA_ARGS__))  
  4. #define ATTR_3(arg, ...) ATTR_1(arg) ATTR(ATTR_2(__VA_ARGS__))  

4、內建巨集

c/c++標準中預定義了幾個巨集,只要編譯器是支援標準的即可以在程式碼中直接使用這些巨集:

__LINE__ // 當前程式碼行的行號
__FILE__ // 源程式的完整路徑
__DATE__ // 系統日期
__TIME__ // 系統時間
__TIMESTAMP__   // 系統時間戳
__FUNCTION__ // 當前程式碼行所在的函式的名字
__STDC__ // 當要求程式嚴格遵循ANSI C標準時該標識被賦值為1
__cplusplus // 當編寫C++程式時該識別符號被定義

另外有一些是編譯器相關的預定義巨集:

VC:_MSC_VER// VC編譯器版本號

更多參考:點選開啟連結

GCC/G++:__GNUC__// GNU編譯器版本號

更多參考:點選開啟連結點選開啟連結

二、常用巨集技巧

1、遍歷變參巨集的每個引數

巨集只是簡單替換的過程,所以不支援任何邏輯判斷語句,但是依然可以用多條巨集來實現相同的功能。

在實現遍歷遍歷每個巨集引數之前,先看看怎麼實現簡單的統計引數的個數。首先編譯器沒有提供任何可以直接使用來計算引數個數的方法,所以需要使用一點技巧來實現這個功能:數軸佔位,即把引數依次放到數軸每個點上,那麼最後一個沒被安放位置上的數就是引數的個數,不過這裡需要顛倒一下佔位,實現:

[cpp] view plain copy
  1. // 假設巨集引數個數上限為10,否則需要手動擴充套件  
  2. #define COUNT_PARMS_IMP(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, NUM, ...) NUM  
  3. #define COUNT_PARMS(...) \  
  4.     ATTR(COUNT_PARMS_IMP(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0))  
利用類似的思路,使用多條巨集語句,來分離出每一個引數,即可以模擬遍歷引數的功能:

[cpp] view plain copy
  1. // 假設巨集引數個數上限為10,否則需要手動擴充套件  
  2. #define ARG_1(arg) printf(arg);  
  3. #define ARG_2(arg, ...) ARG_1(arg) ATTR(ARG_1(__VA_ARGS__))  
  4. #define ARG_3(arg, ...) ARG_1(arg) ATTR(ARG_2(__VA_ARGS__))  
  5. #define ARG_4(arg, ...) ARG_1(arg) ATTR(ARG_3(__VA_ARGS__))  
  6. #define ARG_5(arg, ...) ARG_1(arg) ATTR(ARG_4(__VA_ARGS__))  
  7. #define ARG_6(arg, ...) ARG_1(arg) ATTR(ARG_5(__VA_ARGS__))  
  8. #define ARG_7(arg, ...) ARG_1(arg) ATTR(ARG_6(__VA_ARGS__))  
  9. #define ARG_8(arg, ...) ARG_1(arg) ATTR(ARG_7(__VA_ARGS__))  
  10. #define ARG_9(arg, ...) ARG_1(arg) ATTR(ARG_8(__VA_ARGS__))  
  11. #define ARG_10(arg, ...) ARG_1(arg) ATTR(ARG_9(__VA_ARGS__))  

但是這樣的巨集有個缺點就是在使用時必須明確地指定呼叫有幾個引數的版本,不過有了前面實現的獲取引數個數的巨集,可以借用這個巨集來自動選擇哪個版本的引數遍歷巨集:

[cpp] view plain copy
  1. #define ARGS(...) \  
  2.     ATTR(SYMBOL_CATENATE_WITH_MACRO(ARG_, ATTR(COUNT_PARMS(__VA_ARGS__)))(__VA_ARGS__))  

2、跨平臺程式開發

一些編譯器提供的平臺相關的預定義巨集,可以很方便的用來做跨平臺開發,例如:

[cpp] view plain copy
  1. #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)  
  2.   
  3. // windows  
  4.   
  5. #elif defined(__linux__) || defined(__linux)  
  6.   
  7. // linux  
  8.   
  9. #endif  
更多參考:點選開啟連結點選開啟連結

3、利用預定義巨集除錯程式

__FILE__、__LINE__、__FUNCTION__等可以很方便的獲取程式相關的資訊,當程式出現錯誤時,利用這些巨集可以及時地生成錯誤資訊並輸出到日誌中,以便檢視和除錯。

4、除錯巨集定義

巨集的缺點之一就是難以除錯,一旦巨集體的定義出現問題導致編譯錯誤,編譯器將報一些令人費解的錯誤。不過對於巨集定義導致的編譯錯誤,還是有一些方法除錯的:

(1)、檢視巨集展開後的完整程式碼

VC下可以利用"生成預處理檔案"選項,巨集展開後的程式碼將輸出到.i檔案中,操作參考:點選開啟連結

GCC下使用編譯選項-E即可。

5、巨集超程式設計

參考:點選開啟連結

相關文章