今天看caffe源碼的時候看到了不少宏定義的內容,苦於代碼基礎薄弱,沒法所有理解,故在網上搜得此篇好文,轉載一發
附原文地址:http://blog.csdn.net/hanchaoman/article/details/8809951/ 侵刪~ios
宏替換是C/C++系列語言的技術特點,C/C++語言提供了強大的宏替換功能,源代碼在進入編譯器以前,要先通過一個稱爲「預處理器」的模塊,這個模塊將宏根據編譯參數和實際編碼進行展開,展開後的代碼才正式進入編譯器,進行詞法分析、語法分析等等。算法
咱們經常使用的宏替換主要有這麼幾個類型。
1.宏常量
在ACM等算法競賽中,常常會把數組的最大下標經過宏定義的方法給出,以方便調試,例如:
#define MAX 1000c#
int array[MAX][MAX]
......數組
for(int i = 0; i < MAX; i++)
......編輯器
將一個數字定義成全局的常量,這個用法在國產垃圾教材上十分常見。但在經典著做《Effective C++》中,這種作法卻並不提倡,書中更加推薦以const常量來代替宏常量。由於在進行詞法分析時,宏的引用已經被其實際內容替換,所以宏名不會出如今 符號表中。因此一旦出錯,看到的將是一個無心義的數字,好比上文中的1000,而不是一個有意義的名稱,如上文中的MAX。而const在符號表中會有自 己的位置,所以出錯時能夠看到更加有意義的錯誤提示。ide
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「函數」要比使用指針參數效率還要高,由於它連指針參數都不用傳遞而是使用直接代碼,對於一些效率要求比較明顯的場合,宏仍是首選。
四、取消宏定義
#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就是惟一一處放置條件編譯開關的地方,更有利於維護。
五、注意事項
1)、普通宏定義
(1)宏名通常用大寫
(2)使用宏可提升程序的通用性和易讀性,減小不一致性,減小輸入錯誤和便於修改。
(3)預處理是在編譯以前的處理,而編譯工做的任務之一就是語法檢查,預處理不作語法檢查。
(4)宏定義末尾不加分號;
(5)宏定義寫在函數的花括號外邊,做用域爲其後的程序,一般在文件的最開頭。
(6)能夠用#undef命令終止宏定義的做用域
(7)宏定義能夠嵌套
(8)字符串""中永遠不包含宏
(9)宏定義不分配內存,變量定義分配內存。
2)、帶參宏定義
(1)實參若是是表達式容易出問題
(2)宏名和參數的括號間不能有空格
(3)宏替換隻做替換,不作計算,不作表達式求解
(4)函數調用在編譯後程序運行時進行,而且分配內存。宏替換在編譯前進行,不分配內存
(5)宏的啞實結合不存在類型,也沒有類型轉換。
(6)函數只有一個返回值,利用宏則能夠設法獲得多個值
(7)宏展開使源程序變長,函數調用不會
(8)宏展開不佔運行時間,只佔編譯時間,函數調用佔運行時間(分配內存、保留現場、值傳遞、返回值)
六、關於#和##
在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;
七、關於...的使用
在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合法,將不會產生編譯錯誤。 這裏列出了一些宏使用中容易出錯的地方,以及合適的使用方式。