本文主要寫了預處理指定和#define 宏替換及宏函數,以及爲什麼會用到 do/while(0); 讀者需要那部分知識可以直接點擊目錄裏面的鏈接
本文參考:https://www.cnblogs.com/flowingwind/p/8304668.html
https://www.cnblogs.com/bytebee/p/8205707.html
https://www.cnblogs.com/wuweierzhi/p/11591999.html
目錄
一、預編譯指令
命令 | 命令效果 |
#空指令 | 無任何效果 |
#include | 包含一個源代碼文件 |
#define | 定義宏 |
#undef | 取消已定義的宏 |
#if | 如果給定條件爲真,則編譯下面代碼 |
#ifdef | 如果宏已經定義,則編譯下面代碼 |
#ifndef | 如果宏沒有定義,則編譯下面代碼 |
#elif | 如果前面的#if給定條件不爲真,當前條件爲真,則編譯下面代碼 |
#endif | 結束一個#if……#else條件編譯塊 |
#error | 停止編譯並顯示錯誤信息 |
條件編譯命令最常見的形式爲:
- #ifdef 標識符
- 程序段1
- #else
- 程序段2
- #endif
例:
#ifndef bool
#define ture 1
#define false 0
#endif
在早期vc中bool變量用1,0表示,即可以這麼定義,保證程序的兼容性
在頭文件中使用#ifdef和#ifndef是非常重要的,可以防止雙重定義的錯誤。
//main.cpp文件
#include "cput.h"
#include "put.h"
int main()
{
cput();
put();
cout << "Hello World!" << endl;
return 0;
}
//cput.h 頭文件
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
//put.h頭文件
#include "cput.h"
int put()
{
cput();
return 0;
}
編譯出錯;在main.cpp中兩次包含了cput.h
嘗試模擬還原編譯過程;
當編譯器編譯main.cpp時
//預編譯先將頭文件展開加載到main.cpp文件中
//展開#include "cput.h"內容
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
//展開#include "put.h"內容
//put.h包含了cput.h先展開
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
int put()
{
cput();
return 0;
}
int main()
{
cput();
put();
cout << "Hello World!" << endl;
return 0;
}
很明顯合併展開後的代碼,定義了兩次cput()函數;
如果將cput.h改成下面形式:
#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
#endif
當編譯器編譯main.cpp時合併後的main.cpp文件將會是這樣的:
#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
#endif
#ifndef _CPUT_H_
#define _CPUT_H_
#include <iostream>
using namespace std;
int cput()
{
cout << "Hello World!" << endl;
return 0;
}
#endif
int put()
{
cput();
return 0;
}
int main()
{
cput();
put();
cout << "Hello World!" << endl;
return 0;
}
這次編譯通過運行成功;因爲在展開put.h中包含的cput.h,會不生效,前面已經定義了宏_CPUT_H_
二、宏定義
#define 宏定義,簡單理解就是直接替換。
宏定義可以幫助我們防止出錯,提高代碼的可移植性和可讀性等。
在軟件開發過程中,經常有一些常用或者通用的功能或者代碼段,這些功能既可以寫成函數,也可以封裝成爲宏定義。那麼究竟是用函數好,還是宏定義好?這就要求我們對二者進行合理的取捨。
我們來看一個例子,比較兩個數或者表達式大小,首先我們把它寫成宏定義:
#define MAX(a, b) ((a)>(b) ? (a):(b))
其次,把它用函數來實現:
int max(int a, int b)
{
return ((a > b) ? a : b)
}
很顯然,我們不會選擇用函數來完成這個任務,原因有兩個:
1.函數調用會帶來額外的開銷,它需要開闢一片棧空間,記錄返回地址,將形參壓棧,從函數返回還要釋放堆棧。這種開銷不僅會降低代碼效率,而且代碼量也會大大增加,而使用宏定義則在代碼規模和速度方面都比函數更勝一籌;
2.函數的參數必須被聲明爲一種特定的類型,所以它只能在類型合適的表達式上使用,我們如果要比較兩個浮點型的大小,就不得不再寫一個專門針對浮點型的比較函數。反之,上面的那個宏定義可以用於整形、長整形、單浮點型、雙浮點型以及其他任何可以用“>”操作符比較值大小的類型,也就是說,宏是與類型無關的。
和使用函數相比,使用宏的不利之處在於每次使用宏時,一份宏定義代碼的拷貝都會插入到程序中。除非宏非常短,否則使用宏會大幅度增加程序的長度。
還有一些任務根本無法用函數實現,但是用宏定義卻很好實現。比如參數類型沒法作爲參數傳遞給函數,但是可以把參數類型傳遞給帶參的宏。看下面的例子:
/* \ 表示換行符*/
#define MALLOC(n, type) \
((type *) malloc((n) * sizeof(type)) \*強制類型轉換*\
利用這個宏,我們就可以爲任何類型分配一段我們指定的空間大小,並返回指向這段空間的指針。我們可以觀察一下這個宏確切的工作過程:
int *ptr;
ptr = MALLOC(5, int);
將這宏展開以後的結果:
ptr = (int *)malloc((5) * sizeof(int));
這個例子是宏定義的經典應用之一,完成了函數不能完成的功能,但是宏定義也不能濫用,通常,如果相同的代碼需要出現在程序的幾個地方,更好的方法是把它實現爲一個函數。
example:
define的單行定義:
#define maxi(a,b) ((a>b) ? a:b)
define的多行定義
define可以替代多行的代碼,例如MFC中的宏定義:
#define MACRO(arg1, arg2) do{\
\
stmt1; \
stmt2; \
\
}while(0)
宏定義寫出swap(x,y)交換函數
#define swap(x, y)\
x = x + y; \
y = x - y; \
x = x - y;
zigbee裏多行define有如下例子
#define FillAndSendTxOptions(TRANSSEQ, ADDR, ID, LEN, TxO ){ \
afStatus_t stat; \
ZDP_TxOptions = (TxO); \
stat = fillAndSend( (TRANSSEQ), (ADDR), (ID), (LEN) ); \
ZDP_TxOptions = AF_TX_OPTIONS_NONE; \
return stat; \
}
三、do while(0);
1.幫助定義複雜的宏以避免錯誤
舉例來說,假設你需要定義這樣一個宏:
#define DOSOMETHING(x) foo1(x); foo2(x)
有如下調用語句
DOSOMETHING(value);
這時將宏展開爲:
fool(value); fool(value);
但是如果你在調用的時候這麼寫:
if(a>0)
DOSOMETHING(value);
因爲宏在預處理的時候會直接被展開,你實際上寫的代碼是這個樣子的:
if(a>0)
foo1(value);
foo2(value);
這就出現了問題,因爲無論a是否大於0,foo2(value)都會被執行,導致程序出錯。
那麼僅僅使用{}將foo1(x)和foo2(x)包起來行麼?比如:
#define DOSOMETHING(x) { foo1(x); foo2(x); }
我們在寫代碼的時候都習慣在語句右面加上分號,如果在宏中使用{},代碼編譯展開後宏就相當於這樣寫了:“{...};”,展開後就是這個樣子:
if(a>0)
{
foo1();foo2();
};
很明顯,這是一個語法錯誤(大括號後多了一個分號)。
現在的編譯器會自動檢測自動忽略分號,不會報錯,但是我們還是希望能跑在老的編譯器上。
在沒有do/while(0)的情況下,在所有可能情況下,期望我們寫的多語句宏總能有正確的表現幾乎是不可能的。
如果我們使用do{...}while(0)來定義宏,即:
#define DOSOMETHING(x) do{foo1(x); foo2(x);} while(0) //注意這裏沒有分號
對於上面的if語句,將會被擴展爲:
if(a > 0)
do{foo1(x); foo2(x);} while(0);
這樣,宏被展開後,上面的調用語句纔會保留初始的語義。do能確保大括號裏的邏輯能被執行,而while(0)能確保該邏輯只被執行一次,就像沒有循環語句一樣。
總結:在Linux和其它代碼庫裏的,很多宏實現都使用do/while(0)來包裹他們的邏輯,這樣不管在調用代碼中怎麼使用分號和大括號,而該宏總能確保其行爲是一致的。
cocos2d-x中大量使用了這種宏定義:
#define CC_SAFE_DELETE(p) do { if(p) { delete (p); (p) = 0; } } while(0)
2. 避免使用goto控制程序流(和宏定義無關,屬於代碼優化)
在一些函數中,我們可能需要在return語句之前做一些清理工作,比如釋放在函數開始處由malloc申請的內存空間,使用goto總是一種簡單的方法:
int foo()
{
somestruct *ptr = malloc(...);
dosomething...;
if(error)
goto END;
dosomething...;
if(error)
goto END;
dosomething...;
END:
free(ptr);
return 0;
}
但由於goto不符合軟件工程的結構化,而且有可能使得代碼難懂,所以很多人都不倡導使用,這個時候我們可以使用do{...}while(0)來做同樣的事情:
int foo()
{
somestruct *ptr = malloc(...);
do
{
dosomething...;
if(error)
break;
dosomething...;
if(error)
break;
dosomething...;
}
while(0); //注意這裏有;分號
free(ptr);
return 0;
}
這裏將函數主體部分使用do{...}while(0)包含起來,使用break來代替goto,後續的清理工作在while之後,現在既能達到同樣的效果,而且代碼的可讀性、可維護性都要比上面的goto代碼好的多了。
我經常使用這個種技能在Lua裏,Lua不支持do{...}while(0)語法,但是Lua有一種類似的語法repeat...until,僞代碼如下:
repeat
dosomething...
if error then
break;
end
dosomething...;
if error then
break;
end
dosomething...;
until (1);
print("break repeat");
這樣和do{...}while(0)一樣,也保證了只執行一次,可以用break調出循環。
3. 避免由宏引起的警告
內核中由於不同架構的限制,很多時候會用到空宏,。在編譯的時候,這些空宏會給出warning,爲了避免這樣的warning,我們可以使用do{...}while(0)來定義空宏:
#define EMPTYMICRO do{}while(0)
這種情況不太常見,因爲有很多編譯器,已經支持空宏。
4. 定義單一的函數塊來完成複雜的操作
如果你有一個複雜的函數,變量很多,而且你不想要增加新的函數,可以使用do{...}while(0),將你的代碼寫在裏面,裏面可以定義變量而不用考慮變量名會同函數之前或者之後的重複。
但是我不建議這樣做,儘量聲明不同的變量名,以便於後續開發人員閱讀。
int key;
string value;
int func()
{
int key = GetKey();
string value = GetValue();
dosomething for key,value;
do{
int key;string value;
dosomething for this key,value;
}while(0);
}