C/C++-技巧-宏

 一、宏基礎

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

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

1、宏的分類

宏對象:沒有參數的宏。這類宏常常被用來定義常量,通常比較簡單,例如:

#define MAX_NUM 100
宏函數:帶有參數的宏。這類宏的應用場景很多,比如定義函數、產生代碼等等,隨着用法的不同,難易程度也有很大的波動,例如:

#define MAX(a, b) ((a)>(b) ? (a) : (b))

2、宏的操作符

#:字符串化一個宏參數,即在參數名字前後加上"。例如:

#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')將產生錯誤。

#@:字符化一個宏參數,即在參數名字前後加上'。例如:

#define CHARIZE(arg) #@arg
##:拼接宏參數和另一個符號,即連接兩個符號生成一個新的符號。例如:

#define SYMBOL_CATENATE(arg1, arg2) arg1 ## arg2

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

#define CHARIZE_WITH_MACRO(arg) CHARIZE(arg)
#define SYMBOL_CATENATE_WITH_MACRO(arg1, arg2) SYMBOL_CATENATE(arg1, arg2)

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

#define DEFINE_VARIABLE(name1, name2, type) type name1; \
	type name2;

3、變參宏

宏函數也可以接受個數不定的參數,形參寫爲...,在宏體內獲取形參使用__VA_ARGS__,例如:

#define PRINTF(format, ...) printf(format, __VA_ARGS__);
注意:當__VA_ARGS__作爲宏實參再次被傳入另一個宏函數的時候,在VC下直接編譯時__VA_ARGS__只會被解釋爲一個參數,例如下面代碼:

#define ATTR_1(arg) printf(arg);
#define ATTR_2(arg, ...) ATTR_1(arg) ATTR_1(__VA_ARGS__)
#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():

#define ATTR(args) args
#define ATTR_1(arg) printf(arg);
#define ATTR_2(arg, ...) ATTR_1(arg) ATTR(ATTR_1(__VA_ARGS__))
#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、遍歷變參宏的每個參數

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

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

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

// 假設宏參數個數上限爲10,否則需要手動擴展
#define ARG_1(arg) printf(arg);
#define ARG_2(arg, ...) ARG_1(arg) ATTR(ARG_1(__VA_ARGS__))
#define ARG_3(arg, ...) ARG_1(arg) ATTR(ARG_2(__VA_ARGS__))
#define ARG_4(arg, ...) ARG_1(arg) ATTR(ARG_3(__VA_ARGS__))
#define ARG_5(arg, ...) ARG_1(arg) ATTR(ARG_4(__VA_ARGS__))
#define ARG_6(arg, ...) ARG_1(arg) ATTR(ARG_5(__VA_ARGS__))
#define ARG_7(arg, ...) ARG_1(arg) ATTR(ARG_6(__VA_ARGS__))
#define ARG_8(arg, ...) ARG_1(arg) ATTR(ARG_7(__VA_ARGS__))
#define ARG_9(arg, ...) ARG_1(arg) ATTR(ARG_8(__VA_ARGS__))
#define ARG_10(arg, ...) ARG_1(arg) ATTR(ARG_9(__VA_ARGS__))

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

#define ARGS(...) \
	ATTR(SYMBOL_CATENATE_WITH_MACRO(ARG_, ATTR(COUNT_PARMS(__VA_ARGS__)))(__VA_ARGS__))

2、跨平臺程序開發

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

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)

// windows

#elif defined(__linux__) || defined(__linux)

// linux

#endif
更多參考:點擊打開鏈接點擊打開鏈接

3、利用預定義宏調試程序

__FILE__、__LINE__、__FUNCTION__等可以很方便的獲取程序相關的信息,當程序出現錯誤時,利用這些宏可以及時地生成錯誤信息並輸出到日誌中,以便查看和調試。

4、調試宏定義

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

(1)、查看宏展開後的完整代碼

VC下可以利用"生成預處理文件"選項,宏展開後的代碼將輸出到.i文件中,操作參考:點擊打開鏈接

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

5、宏元編程

參考:點擊打開鏈接


參考:點擊打開鏈接點擊打開鏈接點擊打開鏈接點擊打開鏈接點擊打開鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章