?宏替換是C/C++系列語言的技術特色,C/C++語言提供了強大的宏替換功能,源代碼在進入編譯器之前,要先經過一個稱為“預處理器”的模塊,這個模塊將宏根據編譯參數和實際編碼進行展開,展開后的代碼才正式進入編譯器,進行詞法分析、語法分析等等。
??? 我們常用的宏替換主要有這么幾個類型。
1.宏常量
??? 在ACM等算法競賽中,經常會把數組的最大下標通過宏定義的方法給出,以方便調試,例如:
#define MAX 1000
int array[MAX][MAX]
......
for(int i = 0; i < MAX; i++)
......
??? 將一個數字定義成全局的常量,這個用法在國產垃圾教材上十分常見。但在經典著作《Effective C++》中,這種做法卻并不提倡,書中更加推薦以const常量來代替宏常量。因為在進行詞法分析時,宏的引用已經被其實際內容替換,因此宏名不會出現在符號表中。所以一旦出錯,看到的將是一個無意義的數字,比如上文中的1000,而不是一個有意義的名稱,如上文中的MAX。而const在符號表中會有自己的位置,因此出錯時可以看到更加有意義的錯誤提示。
2.用于條件編譯標識的宏
#define常與#ifdef/#ifndef/defined指令配合使用,用于條件編譯。
#ifndef _HEADER_INC_
#define _HEADER_INC_
……
……
#endif
??? 這種宏標記在頭文件中十分常見,用于防止頭文件被反復包含。應該養成習慣在每個頭文件中都添加這種標記。
??? 還有一種用于條件編譯的用法
#ifdef DEBUG
printf("{“}Debug information\n");
#endif
??? 通過DEBUG宏,我們可以在代碼調試的過程中輸出輔助調試的信息。當DEBUG宏被刪除時,這些輸出的語句就不會被編譯。更重要的是,這個宏可以通過編譯參數來定義。因此通過改變編譯參數,就可以方便的添加和取消這個宏的定義,從而改變代碼條件編譯的結果。
?
在條件編譯時建議使用#if defined和#if !defined來代替使用#ifdef/#ifndef,因為前者更方便處理多分支的情況與較復雜條件表達式的情況。#ifdef/#ifndef只能處理兩個分支:#ifdef/#ifndef,#else,#endfi;#if defined和#if !defined可以處理多分支的情況:#if defined/#if !defined, #elif defined, #else, #endif。#ifdef只能判斷是否定義,但是#if defined可以判斷復雜的表達式的值是否為真。
#if defined(OS_HPUX)&&(defined(HPUX_11_11)|| defined(HPUX_11_23)?
// for HP-UX 11.11 and 11.23?
#elif defined(OS_HPUX) && defined(HPUX_11_31?
// for HP-UX 11.31?
#elif defined(OS_AIX)?
// for AIX?
#else?
…?
#endif
條件編譯時,如果一個文件中太多條件編譯的代碼,有些編輯器的智能感知可能都不能很好地解析,還是保持代碼越簡單越好。對于函數級別的條件編譯主要有兩種實現方式:?
(1) 同一個函數聲明,同一個函數定義,函數體內使用條件編譯代碼。這種方式有個問題,如果條件編譯代碼太多,會導致這個函數體很長,不利于閱讀與維護;有一個優點是,有利于編輯器的智能感知,因為這樣解析函數名比較方便,但隨著編輯器功能的完善,這方面的差別就不明顯了。
(2) 根據編譯條件,將編譯條件相同的代碼放到單獨的文件中,這些文件在頂層文件中使用條件編譯指令來引用。這種方式最大的優點就是不同平臺的程序由不同的源文件來實現,很便于多人分工合作,對于某一部分代碼由一個人實現并測試完成后直接把源文件復制過來就可以了,進行低層次的單元測試非常方便;它的缺點就是增加了目錄中的文件數量。
3.宏函數
??? 宏函數的語法有以下特點:
??? (1)、如果需要換行,則行末要加反斜杠“\”表示換行。宏函數體的最后一行不加反斜杠。
??? (2)、假設有參數ARGU,值為argu,則所有的ARGU被直接替換為argu,#ARGU被認為是字符串,會被替換成"argu"(帶引號)。
??? (3)、由于宏函數是直接替換,所有一般情況下對宏函數的調用時行末不需要加分號。
?
宏函數的作用:
1)、避免函數調用,提高程序效率
常用的就是最大值與最小值的判斷函數,由于函數內容并不多,如果定義為函數在調用比較頻繁的場合會明顯降低程序的效率,其實宏是用空間效率換取了時間效率。如取兩個值的最大值:?
#define MAX(a,b) ((a)<(b) ? (b) : (a))
定義為函數:?
inline int Max(int a, int b)
{
?return a<b ? b : a;
}
定義為模板:?
template <typename T>?
inline T TMax(T a, T b)
{
?return a < b ? b : a ;
}
使用宏函數的優點有兩個:
(1)適用于任何實現了operator<的類型,包括自定義類型;
(2)效率最高。雖然使用inline提示符也將函數或模板定義為內聯的,但這只是一種提示而已,到底編譯器有沒有優化還依賴于編譯器的實現,而使用宏函數則是完全由代碼本身控制。?
需要注意的是,由于宏的本質是直接的文本替換,所以在宏函數的“函數體”內都要把參數使用括號括起來,防止參數是表達式時造成語法錯誤或結果錯誤,如:
#define MIN( a, b) b < a ? b : a?
#define SUM( a, b) a + b?
cout<<MIN(3,5)<<endl;?// 語法錯誤:cout<<b < a ? b : a<<endl;?
int c = SUM(a,b)*2;??// c的期望值:16,實際值:13
2)、引用編譯期數據
上述的這些作用雖然使用宏函數可以取得更好的性能,但如果從功能上講完全可以不使用宏函數,而使用模板函數或普通函數實現,但還有些時候只能通過宏實現。例如,程序中在執行某些操作時可能會失敗,此時要打印出失敗的代碼位置,只能使用宏實現。?
#define SHOW_CODE_LOCATION() cout<<__FILE__<<':'<<__LINE__<<'\n'?
if( 0 != rename("oldFileName", "newFileName") )
{?
?cout<<"failed to move file"<<endl;?
?SHOW_CODE_LOCATION();?
}
雖然宏是簡單的替換,所以在調用宏函數SHOW_CODE_LOCATION時,分號可以直接寫到定義里,也可以寫到調用處,但最好還是寫到調用處,看起來更像是調用了函數,否則看著代碼不倫不類,如:
#define SHOW_CODE_LOCATION() cout<<__FILE__<<':'<<__LINE__<<'\n'?
if( 0 != rename("oldFileName", "newFileName") )
{?
?cout<<"failed to move file"<<endl;?
?SHOW_CODE_LOCATION()
}
3)、do-while的妙用
do-while循環控制語句的特點就是循環體內的語句至少會被執行一次,如果while(…)內的條件始終為0時,循環體內的語句就會被執行且只被執行一次,這樣的執行效果與直接使用循環體內的代碼相同,但這們會得到更多的益處。?
#define SWAP_INT(a, b) do
{\
?int tmp = a; \
?a = b; \
?b = tmp; \
}while(0)
int main( void )?
{?
?int x = 3, y = 4;
?if( x > y )
?{
??SWAP_INT(x, y);
?}
?return 0;
}
通過do-while代碼塊的宏定義我們不僅可以把SWAP_INT像函數一樣用,而且還有優點:?
(1)、在宏定義中可以使用局部變量;?
(2)、在宏定義中可以包含多個語句,但可以當作一條語句使用,如代碼中的if分支語句,如果沒有do-while把多條語句組織成一個代碼塊,則程序的運行結果就不正確,甚至不能編譯。?
其實我們定義的SWAP_INT(a, b)相當于定義了引用參數或指針參數的函數,因為它可以改變實參的值。在C++0X中有了decltype關鍵詞,這種優勢就更顯示了,因為在宏中使用了局部變量必須確定變量的類型,所以這個宏只能用于交換int型的變量值,如果換作其它類型則還必須定義新的宏,如SWAP_FLOAT、SWAP_CHAR等,而通過decltype,我們就可以定義一個萬能的宏。?
#include <iostream>?
using namespace std;?
#define SWAP(a, b) do
{ \
?decltype(a) tmp = a; \
?a = b; \
?b = tmp; \
}while(0)
int main( void )?
{?
?int a = 1, b = 2;?
?float f1 = 1.1f, f2 = 2.2f;?
?SWAP(a, b);?
?SWAP(f1,f2);?
?return 0;?
}
通過宏實現的SWAP“函數”要比使用指針參數效率還要高,因為它連指針參數都不用傳遞而是使用直接代碼,對于一些效率要求比較明顯的場合,宏還是首選。
4、取消宏定義
#undef指令用于取消前面用#define定義的宏,取消后就可以重新定義宏。該指令用的并不多,因為過多的#undef會使代碼維護起來非常困難,一般也只用于配置文件中,用來清除一些#define的開關,保證宏定義的唯一性。
// config.h?
#undef HAS_OPEN_SSL?
#undef HAS_ZLIB?
#if defined(HAS_OPEN_SSL)?
…?
#endif?
#if defined(HAS_ZLIB)?
…?
#endif
將對該頭文件的引用放到所有代碼文件的第一行,就可以保證HAS_OPEN_SSL沒有被定義,即使是在編譯選項里定義過一宏,也會被#undef指令取消,這樣使得config.h就是唯一一處放置條件編譯開關的地方,更有利于維護。
5、注意事項
1)、普通宏定義
(1)宏名一般用大寫
(2)使用宏可提高程序的通用性和易讀性,減少不一致性,減少輸入錯誤和便于修改。
(3)預處理是在編譯之前的處理,而編譯工作的任務之一就是語法檢查,預處理不做語法檢查。
(4)宏定義末尾不加分號;
(5)宏定義寫在函數的花括號外邊,作用域為其后的程序,通常在文件的最開頭。
(6)可以用#undef命令終止宏定義的作用域
(7)宏定義可以嵌套
(8)字符串""中永遠不包含宏
(9)宏定義不分配內存,變量定義分配內存。
2)、帶參宏定義
(1)實參如果是表達式容易出問題
(2)宏名和參數的括號間不能有空格
(3)宏替換只作替換,不做計算,不做表達式求解
(4)函數調用在編譯后程序運行時進行,并且分配內存。宏替換在編譯前進行,不分配內存
(5)宏的啞實結合不存在類型,也沒有類型轉換。
(6)函數只有一個返回值,利用宏則可以設法得到多個值
(7)宏展開使源程序變長,函數調用不會
(8)宏展開不占運行時間,只占編譯時間,函數調用占運行時間(分配內存、保留現場、值傳遞、返回值)
6、關于#和##
在C語言的宏中,#的功能是將其后面的宏參數進行字符串化操作(Stringfication),簡單說就是在對它所引用的宏變量通過替換后在其左右各加上一個雙引號。比如下面代碼中的宏:
#define WARN_IF(EXP)??? \
??? do{ if (EXP)??? \
??????????? fprintf(stderr, "Warning: " #EXP "\n"); }?? \
??? while(0)
那么實際使用中會出現下面所示的替換過程:
WARN_IF (divider == 0);
?被替換為
do {
??? if (divider == 0)
? fprintf(stderr, "Warning" "divider == 0" "\n");
} while(0);
這樣每次divider(除數)為0的時候便會在標準錯誤流上輸出一個提示信息。
而##被稱為連接符(concatenator),用來將兩個Token連接為一個Token。注意這里連接的對象是Token就行,而不一定是宏的變量。比如你要做一個菜單項命令名和函數指針組成的結構體的數組,并且希望在函數名和菜單項命令名之間有直觀的、名字上的關系。那么下面的代碼就非常實用:
struct command
{
?char * name;
?void (*function) (void);
};
#define COMMAND(NAME) { NAME, NAME ## _command }
// 然后你就用一些預先定義好的命令來方便的初始化一個command結構的數組了:
struct command commands[] = {
?COMMAND(quit),
?COMMAND(help),
?...
}
COMMAND宏在這里充當一個代碼生成器的作用,這樣可以在一定程度上減少代碼密度,間接地也可以減少不留心所造成的錯誤。我們還可以n個##符號連接 n+1個Token,這個特性也是#符號所不具備的。比如:
#define LINK_MULTIPLE(a,b,c,d) a##_##b##_##c##_##d
typedef struct _record_type LINK_MULTIPLE(name,company,position,salary);
// 這里這個語句將展開為:
//? typedef struct _record_type name_company_position_salary;
7、關于...的使用
在C宏中稱為Variadic Macro,也就是變參宏。比如:
#define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__)
?// 或者
#define myprintf(templt,args...) fprintf(stderr,templt,args)
第一個宏中由于沒有對變參起名,我們用默認的宏__VA_ARGS__來替代它。第二個宏中,我們顯式地命名變參為args,那么我們在宏定義中就可以用args來代指變參了。同C語言的stdcall一樣,變參必須作為參數表的最有一項出現。當上面的宏中我們只能提供第一個參數templt時,C標準要求我們必須寫成:
myprintf(templt,);
的形式。這時的替換過程為:
myprintf("Error!\n",);
?替換為:
?
fprintf(stderr,"Error!\n",);
這是一個語法錯誤,不能正常編譯。這個問題一般有兩個解決方法。首先,GNU CPP提供的解決方法允許上面的宏調用寫成:
myprintf(templt);
而它將會被通過替換變成:
fprintf(stderr,"Error!\n",);
很明顯,這里仍然會產生編譯錯誤(非本例的某些情況下不會產生編譯錯誤)。除了這種方式外,c99和GNU CPP都支持下面的宏定義方式:
#define myprintf(templt, ...) fprintf(stderr,templt, ##__VAR_ARGS__)
這時,##這個連接符號充當的作用就是當__VAR_ARGS__為空的時候,消除前面的那個逗號。那么此時的翻譯過程如下:
myprintf(templt);
?被轉化為:
fprintf(stderr,templt);
這樣如果templt合法,將不會產生編譯錯誤。 這里列出了一些宏使用中容易出錯的地方,以及合適的使用方式。