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