宏定義函數VS普通函數VS內聯函數
宏定義函數VS普通函數
- 宏定義函數
-
要點:變量都用括號括起來,防止出錯,結尾不需要;。在實際編程中,不推薦把複雜的函數使用宏,不容易調試。多行用\
-
例子:
單行:
#define MAX(a, b) ((a) > (b) ? (a):(b))
多行:#define MALLOC(n, type) \ ((type *) malloc((n)* sizeof(type))
對於第一個函數,如果用普通函數,該怎樣寫?
int max(int a, int b) { return (a > b ? a : b); }
很顯然,我們不會選擇用函數來完成這個任務,原因有兩個:
- 首先,函數調用會帶來額外的開銷,它需要開闢一片棧空間,記錄返回地址,將形參壓棧,從函數返回還要釋放堆棧。這種開銷會降低代碼效率,而使用宏定義則在代碼速度方面比函數更勝一籌;
- 其次,函數的參數必須被聲明爲一種特定的類型,所以它只能在類型合適的表達式上使用,我們如果要比較兩個浮點型的大小,就不得不再寫一個專門針對浮點型的比較函數。反之,上面的那個宏定義可以用於整形、長整形、單浮點型、雙浮點型以及其他任何可以用“>”操作符比較值大小的類型,也就是說,宏是與類型無關的。
- 和使用函數相比,使用宏的不利之處在於每次使用宏時,一份宏定義代碼的拷貝都會插入到程序中。除非宏非常短,否則使用宏會大幅度增加程序的長度。
- 還有一些任務根本無法用函數實現,但是用宏定義卻很好實現。比如參數類型沒法作爲參數傳遞給函數,但是可以把參數類型傳遞給帶參的宏。比如上面的
malloc
的例子.
-
總結:
屬性 | #define宏 | 函數 |
---|---|---|
代碼長度 | 每次使用時,宏代碼都被插入到程序中。除了非常小的宏之外,程序的長度將大幅度增長。 | 函數代碼只出現於一個地方:每次使用這個函數時,都調用那個地方的同一份代碼 |
執行速度 | 更快 | 存在函數調用、返回的額外開銷 |
操作符優先級 | 宏參數的求值是在所有周圍表達式的上下文環境裏,除非它們加上括號,否則鄰近操作符的優先級可能產生不可預料的結果。 | 函數參數只在函數調用時求值一次,它的結果值傳遞給函數。表達式的求值結果更容易預測。 |
參數求值 | 參數用於宏定義時,每次都將重新求值,由於多次求值,具有副作用的參數可能會產生不可預測的結果。 | 參數在函數調用前只求值一次,在函數中多次使用參數並不會導致多次求值過程,參數的副作用並不會造成任何特殊問題。 |
參數類型 | 宏與類型無關,只要參數的操作是合法的,它可以用於任何參數類型。 | 函數的參數是與類型有關係的,如果參數的類型不同,就需要使用不同的函數,即使它們執行的任務是相同的。 |
- 補充: 普通函數的調用堆棧情況.
比如:
P( ),Q( ),R( )在經過編譯器變爲彙編後, 都有自己的代碼位置,比如void P(){ phase 1; Q(); phase 2; } void Q() { phase 3; R(); phase 4; }
而對應的棧幀呢:0x0000400A P: xxx #phase1 call Q xxx #phase2 ret 0x0000400B Q: yyy #phase3 call R yyy #phase4 0x0000400C R: zzz #phase zzz #phase
當R函數調用完畢,則R的棧幀出棧,同時將返回地址2pop,返回給rip,這樣就繼續執行一下條語句了.Q調用完畢也是同理的,所以這就是普通函數的調用情況.--- P的參數入棧 返回地址1(phase2的地址,即函數Q後的一下條語句) --- Q的參數入棧 返回地址2(phas4的地址,即函數R後的一下條語句) --- R的參數 ---
很顯然,普通函數調用在彙編後,這個函數只有一份代碼量,調用它就入棧出棧它的地址,造成一定的開銷.而對於宏定義
來說,只是簡單的文本替換,這在預處理時就替換過了,故實際上你可以認爲就沒有宏定義出來的函數
,所以當然不會有這樣的開銷.
普通函數VS內聯函數 內聯函數VS宏
例子:
inline void P() {
phase P1;
phase P2;
}
void Q() {
phase1;
P();
phase2;
}
P
是一個內聯函數,對於Q來說編譯後得到的彙編是什麼樣子呢?
Q:
xxx #phase 1
yyy #phase P1
zzz #phase P2
xxx #pahse 2
我們可以發現,Q的彙編代碼中並沒有對P的調用(call
),而是直接將P的代碼拷到Q中,這樣的好處就是由於沒有call ret
,所以就沒有普通函數調用帶來的出棧入棧開銷,速度更快,但增加代碼量,增加內存開銷
.
那麼內聯函數
和宏
又有哪些不同?
內聯函數
本質上還是一個函數,只是在調用時不必call ret
罷了.
- 內聯函數是一個真正的函數,遵循函數的類型和作用域規則,編譯器在調用一個內聯函數時,會首先檢查它的參數的類型,保證調用正確,這樣就消除了它的隱患和侷限性;
即:inline void func(int i,int j);
調用這個時候,我必須符合參數類型才能正確調用,而宏直接替換就完事了,是類型無關的. - 內聯函數可以作爲某個類的成員函數,這樣就可以在其中使用所在類的保護成員及私有成員;
- 宏是不加任何驗證的簡單代碼替換,除非萬不得已,不要使用。
注意:
內聯是以代碼膨脹(複製)爲代價,僅僅省去了函數調用的開銷,從而提高函數的執行效率。如果執行函數代碼的時間比處理函數調用機制的時間長,則節省的時間佔比很小;如果代碼執行時間很短,則內聯函數就可以節省函數調用的時間。
- 含有遞歸調用的函數不能設置爲inline;
- 使用了複雜流程控制語句:循環語句和switch語句,不能設置爲inline;
- 由於inline增加體積的特性,所以建議inline函數內的代碼應很短小。最好不超過5行;
- 內聯函數應該在頭文件中定義,關鍵字inline 必須與函數定義體放在一起才能使函數成爲內聯,僅將inline 放在函數聲明前面不起任何作用。