1. 程式人生 > >Effective C++筆記之十五:inline函式的裡裡外外

Effective C++筆記之十五:inline函式的裡裡外外

1.inline函式簡介
inline函式是由inline關鍵字來定義,引入inline函式的主要原因是用它替代C中複雜易錯不易維護的巨集函式。

2.編譯器對inline函式的處理辦法
inline對於編譯器而言,在編譯階段完成對inline函式的處理。將呼叫動作替換為函式的本體。但是它只是一種建議,編譯器可以去做,也可以不去做。從邏輯上來說,編譯器對inline函式的處理步驟一般如下: 
(1)將inline函式體複製到inline函式呼叫點處; 
(2)為所用inline函式中的區域性變數分配記憶體空間; 
(3)將inline函式的的輸入引數和返回值對映到呼叫方法的區域性變數空間中; 
(4)如果inline函式有多個返回點,將其轉變為inline函式程式碼塊末尾的分支(使用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函式程式碼類似於如下形式:

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;  
    }
}  
經過以上處理,可消除所有與呼叫相關的痕跡以及效能的損失。inline通過消除呼叫開銷來提升效能。

3.inline函式使用的一般方法
函式定義時,在返回型別前加上關鍵字inline即把函式指定為內聯,函式申明時可加也可不加。但是建議函式申明的時候,也加上inline,這樣能夠達到”程式碼即註釋”的作用。

使用格式如下:

inline int functionName(int first, int secend,...) {/****/};
inline如果只修飾函式的申明的部分,如下風格的函式foo不能成為行內函數:

inline void foo(int x, int y); //inline僅與函式宣告放在一起
 
void foo(int x, int y){}
而如下風格的函式foo 則成為行內函數:

void foo(int x, int y);
 
inline void foo(int x, int y){} //inline與函式定義體放在一起
4.inline函式的優點與缺點

從上面可以知道,inline函式相對巨集函式有如下優點: 
(1)行內函數同巨集函式一樣將在被呼叫處進行程式碼展開,省去了引數壓棧、棧幀開闢與回收,結果返回等,從而提高程式執行速度。

(2)行內函數相比巨集函式來說,在程式碼展開時,會做安全檢查或自動型別轉換(同普通函式),而巨集定義則不會。 

例如巨集函式和行內函數:

//巨集函式
#define MAX(a,b) ((a)>(b)?(a):(b))
 
//行內函數
inline int MAX(int a,int b)
{
    return a>b?a:b;
}
(3)在類中宣告同時定義的成員函式,自動轉化為行內函數,因此行內函數可以訪問類的成員變數,巨集定義則不能。

(4)行內函數在執行時可除錯,而巨集定義不可以。

萬事萬物都有陰陽兩面,行內函數也不外乎如此,使用inline函式,也要三思慎重。inline函式的缺點總結如下: 
(1)程式碼膨脹。 
inline函式帶來的執行效率是典型的以空間換時間的做法。內聯是以程式碼膨脹(複製)為代價,消除函式呼叫帶來的開銷。如果執行函式體內程式碼的時間,相比於函式呼叫的開銷較大,那麼效率的收穫會很少。另一方面,每一處行內函數的呼叫都要複製程式碼,將使程式的總程式碼量增大,消耗更多的記憶體空間。

(2)inline函式無法隨著函式庫升級而升級。 
如果f是函式庫中的一個inline函式,使用它的使用者會將f函式實體編譯到他們的程式中。一旦函式庫實現者改變f,所有用到f的程式都必須重新編譯。如果f是non-inline的,使用者程式只需重新連線即可。如果函式庫採用的是動態連線,那這一升級的f函式可以不知不覺的被程式使用。

(3)是否內聯,程式設計師不可控。 
inline函式只是對編譯器的建議,是否對函式內聯,決定權在於編譯器。編譯器認為呼叫某函式的開銷相對該函式本身的開銷而言微不足道或者不足以為之承擔程式碼膨脹的後果則沒必要內聯該函式,若函數出現遞迴,有些編譯器則不支援將其內聯。

5.inline函式的注意事項
(1)使用函式指標呼叫行內函數將會導致內聯失敗。 
也就是說,如果使用函式指標來呼叫行內函數,那麼就需要獲取inline函式的地址。如果要取得一個inline函式的地址,編譯器就必須為此函式產生一個函式實體,那麼就內聯失敗。

(2)如果函式體程式碼過長或者有多重迴圈語句,if或witch分支語句或遞迴時,不宜用內聯。

(3)類的constructors、destructors和虛擬函式往往不是inline函式的最佳選擇。 
類的建構函式(constructors)可能需要呼叫父類的建構函式,解構函式同樣可能需要呼叫父類的解構函式,二者背後隱藏著大量的程式碼,不適合作為inline函式。虛擬函式(destructors)往往是執行時確定的,而inline是在編譯時進行的,所以內聯虛擬函式往往無效。如果直接用類的物件來使用虛擬函式,那麼對有的編譯器而言,也可起到優化作用。

(4)至於行內函數是定義在標頭檔案還是原始檔的建議。 

內聯展開是在編譯時進行的,只有連結的時候原始檔之間才有關係。所以內聯要想跨原始檔必須把實現寫在標頭檔案裡。如果一個inline函式會在多個原始檔中被用到,那麼必須把它定義在標頭檔案中。參考如下示例:

// 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
上面這種錯誤,就是因為行內函數fun()定義在編譯單元base.cpp中,那麼其他編譯單元中呼叫fun()的地方將無法解析該符號,因為在編譯單元base.cpp生成目標檔案base.obj後,行內函數fun()已經被替換掉,編譯器不會為fun()生成函式實體,連結器自然無法解析。所以如果一個inline函式會在多個原始檔中被用到,那麼必須把它定義在標頭檔案中。

這裡有個問題,當在標頭檔案中定義行內函數,那麼被多個原始檔包含時,如果編譯器因為inline函式不適合被內聯時,拒絕將inline函式進行內聯處理,那麼多個原始檔在編譯生成目標檔案後都將各自保留一份inline函式的實體,這個時候程式在連線階段就會出現重定義錯誤。解決辦法是在需要inline的函式使用static。

//test.h
static inline int max(int a,int b)
{
    return a>b?a:b;
}
事實上,inline函式具有內部連結特性,所以如果實際上沒有被內聯處理,也不會報重定義錯誤,因此使用static修飾inline函式有點多餘。

(5)能否強制編譯器進行內聯操作? 
也有人可能會覺得能否強制編譯器進行函式內聯,而不是建議編譯器進行內聯呢?很不幸的是目前還不能強制編譯器進行函式內聯,如果使用的是MSVC++, 注意__forceinline如同inine一樣,也是一個用詞不當的表現,它只是對編譯器的建議比inline更加強烈,並不能強制編譯器進行inline操作。

(6)如何檢視函式是否被內聯處理了? 
實際在VS2012中預處理了一下,檢視預處理後的.i檔案,inline函式的內聯處理不是在預處理階段,而是在編譯階段。編譯原始檔為彙編程式碼或者反彙編檢視有沒有相關的函式呼叫call,如果沒有就是被inline了。具體可以參考here。

(7)C++類成員函式定義在類體內為什麼不會報重定義錯誤? 
類成員函式定義在類體內,並隨著類的定義放在標頭檔案中,當被不同的原始檔包含,那麼每個原始檔都應該包含了類成員函式的實體,為何在連結的過程中不會報函式的重定義錯誤呢?

原因是:在類裡定義時,這種函式會被編譯器編譯成行內函數,在類外定義的函式則不會。行內函數的好處是加快程式的執行速度,缺點是會增加程式的尺寸。比較推薦的寫法是把一個經常要用的而且實現起來比較簡單的小型函式放到類裡去定義,大型函式最好還是放到類外定義。

可能存在的疑問:類體內的成員函式被編譯器內聯處理,但並不是所有的成員函式都會被內聯處理,比如包含遞迴的成員函式。但是實際測試,將包含遞迴的成員函式定義在類體內,被不同的原始檔包含並不會報重定義錯誤,為什麼會這樣呢?請保持著疑問與好奇心,請繼續往下看。

如果編譯器發現被定義在類體內的成員函式無法被內聯處理,也不會出現重定義的錯誤,因為C++中存在5種作用域的級別,分別是檔案域(全域性作用域)、名稱空間域、類域、函式作用域和程式碼塊作用域(區域性域)。當類成員函式被定義在類體內,那麼其作用域也就被限制在類域,當然定義在類體外的函式作用域也是屬於類域的。顯然並不是因為作用域的原因而不會產生重定義的錯誤。

那麼原因究竟是什麼呢?其實很簡單,類體內定義的成員函式就是inline函式,即使不被內聯處理,inline函式的特性就是不具有外部連線性。所以並不會與其他原始檔中的同名類域中的成員函式發生衝突,也就不會造成重定義的錯誤。

6.小結
可以將內聯理解為C++中對於函式專有的巨集,對於C的函式巨集的一種改進。對於常量巨集,C++提供const替代;而對於函式巨集,C++提供的方案則是inline。C++ 通過內聯機制,既具備巨集程式碼的效率,又增加了安全性,還可以自由操作類的資料成員,算是一個比較完美的解決方案。

上面的結論和觀點,缺乏實踐和權威資料支撐,難免存在錯誤,僅供參考學習,如果大家發現錯誤和需要改進的地方,請大家留言給予寶貴的建議。
--------------------- 
作者:燦哥哥 
來源:CSDN 
原文:https://blog.csdn.net/caoshangpa/article/details/80368193?utm_source=copy 
版權宣告:本文為博主原創文章,轉載請附上博文連結!