Effective C++筆記之十五:inline函數(shù)的里里外外
1.inline函數(shù)簡介
inline函數(shù)是由inline關(guān)鍵字來定義,引入inline函數(shù)的主要原因是用它替代C中復雜易錯不易維護的宏函數(shù)。
2.編譯器對inline函數(shù)的處理辦法
inline對于編譯器而言,在編譯階段完成對inline函數(shù)的處理。將調(diào)用動作替換為函數(shù)的本體。但是它只是一種建議,編譯器可以去做,也可以不去做。從邏輯上來說,編譯器對inline函數(shù)的處理步驟一般如下:?
(1)將inline函數(shù)體復制到inline函數(shù)調(diào)用點處;?
(2)為所用inline函數(shù)中的局部變量分配內(nèi)存空間;?
(3)將inline函數(shù)的的輸入?yún)?shù)和返回值映射到調(diào)用方法的局部變量空間中;?
(4)如果inline函數(shù)有多個返回點,將其轉(zhuǎn)變?yōu)閕nline函數(shù)代碼塊末尾的分支(使用GOTO)。
比如如下代碼:
//求0-9的平方 inline?int?inlineFunc(int?num) {?? ??if(num>9||num<0) ??????return?-1;?? ??return?num*num;?? }?? int?main(int?argc,char*?argv[]) { ????int?a=8; ????int?res=inlineFunc(a); ????cout<<"res:"<<res<<endl; }
inline之后的main函數(shù)代碼類似于如下形式:
int?main(int?argc,char*?argv[]) { ????int?a=8; ????{?? ????????int?_temp_b=8;?? ????????int?_temp;?? ????????if?(_temp_q?>9||_temp_q<0)?_temp?=?-1;?? ????????else?_temp?=_temp*_temp;?? ????????b?=?_temp;?? ????} }
經(jīng)過以上處理,可消除所有與調(diào)用相關(guān)的痕跡以及性能的損失。inline通過消除調(diào)用開銷來提升性能。
3.inline函數(shù)使用的一般方法
函數(shù)定義時,在返回類型前加上關(guān)鍵字inline即把函數(shù)指定為內(nèi)聯(lián),函數(shù)申明時可加也可不加。但是建議函數(shù)申明的時候,也加上inline,這樣能夠達到”代碼即注釋”的作用。
使用格式如下:
inline?int?functionName(int?first,?int?secend,...)?{/****/};
inline如果只修飾函數(shù)的申明的部分,如下風格的函數(shù)foo不能成為內(nèi)聯(lián)函數(shù):
inline?void?foo(int?x,?int?y);?//inline僅與函數(shù)聲明放在一起 void?foo(int?x,?int?y){}
而如下風格的函數(shù)foo 則成為內(nèi)聯(lián)函數(shù):
void?foo(int?x,?int?y); inline?void?foo(int?x,?int?y){}?//inline與函數(shù)定義體放在一起
4.inline函數(shù)的優(yōu)點與缺點
從上面可以知道,inline函數(shù)相對宏函數(shù)有如下優(yōu)點:?
(1)內(nèi)聯(lián)函數(shù)同宏函數(shù)一樣將在被調(diào)用處進行代碼展開,省去了參數(shù)壓棧、棧幀開辟與回收,結(jié)果返回等,從而提高程序運行速度。
(2)內(nèi)聯(lián)函數(shù)相比宏函數(shù)來說,在代碼展開時,會做安全檢查或自動類型轉(zhuǎn)換(同普通函數(shù)),而宏定義則不會。?
例如宏函數(shù)和內(nèi)聯(lián)函數(shù):
//宏函數(shù) #define?MAX(a,b)?((a)>(b)?(a):(b)) //內(nèi)聯(lián)函數(shù) inline?int?MAX(int?a,int?b) { ????return?a>b?a:b; }
使用宏函數(shù)時,其書寫語法也較為苛刻,如果對宏函數(shù)出現(xiàn)如下錯誤的調(diào)用,MAX(a,"Hello");
?宏函數(shù)會錯誤地比較int和字符串,沒有參數(shù)類型檢查。但是使用內(nèi)聯(lián)函數(shù)的時候,會出現(xiàn)類型不匹配的編譯錯誤。
(3)在類中聲明同時定義的成員函數(shù),自動轉(zhuǎn)化為內(nèi)聯(lián)函數(shù),因此內(nèi)聯(lián)函數(shù)可以訪問類的成員變量,宏定義則不能。
(4)內(nèi)聯(lián)函數(shù)在運行時可調(diào)試,而宏定義不可以。
萬事萬物都有陰陽兩面,內(nèi)聯(lián)函數(shù)也不外乎如此,使用inline函數(shù),也要三思慎重。inline函數(shù)的缺點總結(jié)如下:?
(1)代碼膨脹。?
inline函數(shù)帶來的運行效率是典型的以空間換時間的做法。內(nèi)聯(lián)是以代碼膨脹(復制)為代價,消除函數(shù)調(diào)用帶來的開銷。如果執(zhí)行函數(shù)體內(nèi)代碼的時間,相比于函數(shù)調(diào)用的開銷較大,那么效率的收獲會很少。另一方面,每一處內(nèi)聯(lián)函數(shù)的調(diào)用都要復制代碼,將使程序的總代碼量增大,消耗更多的內(nèi)存空間。
(2)inline函數(shù)無法隨著函數(shù)庫升級而升級。?
如果f是函數(shù)庫中的一個inline函數(shù),使用它的用戶會將f函數(shù)實體編譯到他們的程序中。一旦函數(shù)庫實現(xiàn)者改變f,所有用到f的程序都必須重新編譯。如果f是non-inline的,用戶程序只需重新連接即可。如果函數(shù)庫采用的是動態(tài)連接,那這一升級的f函數(shù)可以不知不覺的被程序使用。
(3)是否內(nèi)聯(lián),程序員不可控。?
inline函數(shù)只是對編譯器的建議,是否對函數(shù)內(nèi)聯(lián),決定權(quán)在于編譯器。編譯器認為調(diào)用某函數(shù)的開銷相對該函數(shù)本身的開銷而言微不足道或者不足以為之承擔代碼膨脹的后果則沒必要內(nèi)聯(lián)該函數(shù),若函數(shù)出現(xiàn)遞歸,有些編譯器則不支持將其內(nèi)聯(lián)。
5.inline函數(shù)的注意事項
了解了內(nèi)聯(lián)函數(shù)的優(yōu)缺點,在使用內(nèi)聯(lián)函數(shù)時,我們也要注意以下幾個事項和建議。
(1)使用函數(shù)指針調(diào)用內(nèi)聯(lián)函數(shù)將會導致內(nèi)聯(lián)失敗。?
也就是說,如果使用函數(shù)指針來調(diào)用內(nèi)聯(lián)函數(shù),那么就需要獲取inline函數(shù)的地址。如果要取得一個inline函數(shù)的地址,編譯器就必須為此函數(shù)產(chǎn)生一個函數(shù)實體,那么就內(nèi)聯(lián)失敗。
(2)如果函數(shù)體代碼過長或者有多重循環(huán)語句,if或witch分支語句或遞歸時,不宜用內(nèi)聯(lián)。
(3)類的constructors、destructors和虛函數(shù)往往不是inline函數(shù)的最佳選擇。?
類的構(gòu)造函數(shù)(constructors)可能需要調(diào)用父類的構(gòu)造函數(shù),析構(gòu)函數(shù)同樣可能需要調(diào)用父類的析構(gòu)函數(shù),二者背后隱藏著大量的代碼,不適合作為inline函數(shù)。虛函數(shù)(destructors)往往是運行時確定的,而inline是在編譯時進行的,所以內(nèi)聯(lián)虛函數(shù)往往無效。如果直接用類的對象來使用虛函數(shù),那么對有的編譯器而言,也可起到優(yōu)化作用。
(4)至于內(nèi)聯(lián)函數(shù)是定義在頭文件還是源文件的建議。?
內(nèi)聯(lián)展開是在編譯時進行的,只有鏈接的時候源文件之間才有關(guān)系。所以內(nèi)聯(lián)要想跨源文件必須把實現(xiàn)寫在頭文件里。如果一個inline函數(shù)會在多個源文件中被用到,那么必須把它定義在頭文件中。參考如下示例:
//?base.h class?Base{protected:void?fun();}; //?base.cpp #include?base.h inline?void?Base::fun(){} //derived.h #include?base.h class?Derived:?public?Base{public:void?g();}; //?derived.cpp void?Derived::g(){fun();}?//VC2010:?error?LNK2019:?unresolved?external?symbol
上面這種錯誤,就是因為內(nèi)聯(lián)函數(shù)fun()定義在編譯單元base.cpp中,那么其他編譯單元中調(diào)用fun()的地方將無法解析該符號,因為在編譯單元base.cpp生成目標文件base.obj后,內(nèi)聯(lián)函數(shù)fun()已經(jīng)被替換掉,編譯器不會為fun()生成函數(shù)實體,鏈接器自然無法解析。所以如果一個inline函數(shù)會在多個源文件中被用到,那么必須把它定義在頭文件中。
這里有個問題,當在頭文件中定義內(nèi)聯(lián)函數(shù),那么被多個源文件包含時,如果編譯器因為inline函數(shù)不適合被內(nèi)聯(lián)時,拒絕將inline函數(shù)進行內(nèi)聯(lián)處理,那么多個源文件在編譯生成目標文件后都將各自保留一份inline函數(shù)的實體,這個時候程序在連接階段就會出現(xiàn)重定義錯誤。解決辦法是在需要inline的函數(shù)使用static。
//test.h static?inline?int?max(int?a,int?b) { ????return?a>b?a:b; }
事實上,inline函數(shù)具有內(nèi)部鏈接特性,所以如果實際上沒有被內(nèi)聯(lián)處理,也不會報重定義錯誤,因此使用static修飾inline函數(shù)有點多余。
(5)能否強制編譯器進行內(nèi)聯(lián)操作??
也有人可能會覺得能否強制編譯器進行函數(shù)內(nèi)聯(lián),而不是建議編譯器進行內(nèi)聯(lián)呢?很不幸的是目前還不能強制編譯器進行函數(shù)內(nèi)聯(lián),如果使用的是MSVC++, 注意__forceinline如同inine一樣,也是一個用詞不當?shù)谋憩F(xiàn),它只是對編譯器的建議比inline更加強烈,并不能強制編譯器進行inline操作。
(6)如何查看函數(shù)是否被內(nèi)聯(lián)處理了??
實際在VS2012中預處理了一下,查看預處理后的.i文件,inline函數(shù)的內(nèi)聯(lián)處理不是在預處理階段,而是在編譯階段。編譯源文件為匯編代碼或者反匯編查看有沒有相關(guān)的函數(shù)調(diào)用call,如果沒有就是被inline了。具體可以參考here。
(7)C++類成員函數(shù)定義在類體內(nèi)為什么不會報重定義錯誤??
類成員函數(shù)定義在類體內(nèi),并隨著類的定義放在頭文件中,當被不同的源文件包含,那么每個源文件都應(yīng)該包含了類成員函數(shù)的實體,為何在鏈接的過程中不會報函數(shù)的重定義錯誤呢?
原因是:在類里定義時,這種函數(shù)會被編譯器編譯成內(nèi)聯(lián)函數(shù),在類外定義的函數(shù)則不會。內(nèi)聯(lián)函數(shù)的好處是加快程序的運行速度,缺點是會增加程序的尺寸。比較推薦的寫法是把一個經(jīng)常要用的而且實現(xiàn)起來比較簡單的小型函數(shù)放到類里去定義,大型函數(shù)最好還是放到類外定義。
可能存在的疑問:類體內(nèi)的成員函數(shù)被編譯器內(nèi)聯(lián)處理,但并不是所有的成員函數(shù)都會被內(nèi)聯(lián)處理,比如包含遞歸的成員函數(shù)。但是實際測試,將包含遞歸的成員函數(shù)定義在類體內(nèi),被不同的源文件包含并不會報重定義錯誤,為什么會這樣呢?請保持著疑問與好奇心,請繼續(xù)往下看。
如果編譯器發(fā)現(xiàn)被定義在類體內(nèi)的成員函數(shù)無法被內(nèi)聯(lián)處理,也不會出現(xiàn)重定義的錯誤,因為C++中存在5種作用域的級別,分別是文件域(全局作用域)、命名空間域、類域、函數(shù)作用域和代碼塊作用域(局部域)。當類成員函數(shù)被定義在類體內(nèi),那么其作用域也就被限制在類域,當然定義在類體外的函數(shù)作用域也是屬于類域的。顯然并不是因為作用域的原因而不會產(chǎn)生重定義的錯誤。
那么原因究竟是什么呢?其實很簡單,類體內(nèi)定義的成員函數(shù)就是inline函數(shù),即使不被內(nèi)聯(lián)處理,inline函數(shù)的特性就是不具有外部連接性。所以并不會與其他源文件中的同名類域中的成員函數(shù)發(fā)生沖突,也就不會造成重定義的錯誤。
6.小結(jié)
可以將內(nèi)聯(lián)理解為C++中對于函數(shù)專有的宏,對于C的函數(shù)宏的一種改進。對于常量宏,C++提供const替代;而對于函數(shù)宏,C++提供的方案則是inline。C++ 通過內(nèi)聯(lián)機制,既具備宏代碼的效率,又增加了安全性,還可以自由操作類的數(shù)據(jù)成員,算是一個比較完美的解決方案。
上面的結(jié)論和觀點,缺乏實踐和權(quán)威資料支撐,難免存在錯誤,僅供參考學習,如果大家發(fā)現(xiàn)錯誤和需要改進的地方,請大家留言給予寶貴的建議。
參考文獻
[1]inline函數(shù)?
[2]小問題大思考之C++里的inline函數(shù)?
[3]把inline函數(shù)的定義放在頭文件中?
[4]Inline Functions (C++)?
[5]Can I selectively (force) inline a function??
[6]C語言inline詳細講解?
[7]C++中的作用域與生命周期?
[8]內(nèi)聯(lián)函數(shù)到底有沒有被嵌入到調(diào)用處呢?