淺談C++中內聯關鍵字inline

一、在C&C++中inline 關鍵字用來定義一個類的內聯函數,引入它的主要原因是用它替代C中表達式形式的宏定義。
    表達式形式的宏定義一例:   
    #define ExpressionName(Var1,Var2) ((Var1)+(Var2))*((Var1)-(Var2))爲什麼要取代這種形式呢,且聽我道來:
    1. 首先談一下在C中使用這種形式宏定義的原因,C語言是一個效率很高的語言,這種宏定義在形式及使用上像一個函數,但它使用預處理器實現,沒有了參數壓棧,代碼生成 等一系列的操作,因此,效率很高,這是它在C中被使用的一個主要原因。

    2. 這種宏定義在形式上類似於一個函數,但在使用它時,僅僅只是做預處理器符號表中的簡單替換,因此它不能進行參數有效性的檢測,也就不能享受C++編譯器嚴格類型檢查的好處,另外它的返回值也不能被強制轉換爲可轉換的合適的類型,這樣,它的使用就存在着一系列的隱患和侷限性。

    3. 在C++中引入了類及類的訪問控制,這樣,如果一個操作或者說一個表達式涉及到類的保護成員或私有成員,你就不可能使用這種宏定義來實現(因爲無法將this指針放在合適的位置)。

    4. inline 推出的目的,也正是爲了取代這種表達式形式的宏定義,它消除了它的缺點,同時又很好地繼承了它的優點。

 

二、爲什麼inline能很好地取代預定義呢?
  對應於上面的1-3點,闡述如下:   
    1. inline 定義的類的內聯函數,函數的代碼被放入符號表中,在使用時直接進行替換,(像宏一樣展開),沒有了調用的開銷,效率也很高。

    2.  很明顯,類的內聯函數也是一個真正的函數,編譯器在調用一個內聯函數時,會首先檢查它的參數的類型,保證調用正確。然後進行一系列的相關檢查,就像對待任何一個真正的函數一樣。這樣就消除了它的隱患和侷限性。

    3. inline 可以作爲某個類的成員函數,當然就可以在其中使用所在類的保護成員及私有成員。在何時使用inline函數:首先,你可以使用inline函數完全取代表達式形式的宏定義。另外要注意,內聯函數一般只會用在函數內容非常簡單的時候,這是因爲,內聯函數的代碼會在任何調用它的地方展開,如果函數太複雜,代碼膨脹帶來的惡果很可能會大於效率的提高帶來的益處。內聯函數最重要的使用地方是用於類的存取函數。


例如下面一個簡單的比較函數:

int max(int a, int b)
{
  return a > b ? a : b;
}

爲這麼一個小的操作定義一個函數的好處有:

① 閱讀和理解函數 max 的調用,要比讀一條等價的條件表達式並解釋它的含義要容易得多

② 如果需要做任何修改,修改函數要比找出並修改每一處等價表達式容易得多

③ 使用函數可以確保統一的行爲,每個測試都保證以相同的方式實現

④ 函數可以重用,不必爲其他應用程序重寫代碼

雖然有這麼多好處,但是寫成函數有一個潛在的缺點:調用函數比求解等價表達式要慢得多。在大多數的機器上,調用函數都要做很多工作:調用前要先保存寄存器,並在返回時恢復,複製實參,程序還必須轉向一個新位置執行。C++中支持內聯函數,其目的是爲了提高函數的執行效率,用關鍵字 inline 放在函數定義(注意是定義而非聲明,下文繼續講到)的前面即可將函數指定爲內聯函數,內聯函數通常就是將它在程序中的每個調用點上“內聯地”展開,假設我們將 max 定義爲內聯函數:

則調用: cout<<max(a, b)<<endl;

在編譯時展開爲: cout<<(a > b ? a : b)<<endl;

從而消除了把 max寫成函數的額外執行開銷。

無論是《Effective C++》中的 “Prefer consts,enums,and inlines to #defines” 條款,還是《高質量程序設計指南——C++/C語言》中的“用函數內聯取代宏”,宏在C++中基本是被廢了,在書《高質量程序設計指南——C++/C語言》中這樣解釋到:


關鍵字 inline 必須與函數定義體放在一起才能使函數成爲內聯,僅將 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 與函數定義體放在一起
{
  ...

定義在類聲明之中的成員函數將自動地成爲內聯函數,例如:

class A
{  
   public:
void Foo(int x, int y) { ... }   // 自動地成爲內聯函數  
}

 

但是編譯器是否將它真正內聯則要看 Foo函數如何定義

內聯函數應該在頭文件中定義,這一點不同於其他函數。編譯器在調用點內聯展開函數的代碼時,必須能夠找到 inline 函數的定義才能將調用函數替換爲函數代碼,而對於在頭文件中僅有函數聲明是不夠的。

當然內聯函數定義也可以放在源文件中,但此時只有定義的那個源文件可以用它,而且必須爲每個源文件拷貝一份定義(即每個源文件裏的定義必須是完全相同的),當然即使是放在頭文件中,也是對每個定義做一份拷貝,只不過是編譯器替你完成這種拷貝罷了。但相比於放在源文件中,放在頭文件中既能夠確保調用函數是定義是相同的,又能夠保證在調用點能夠找到函數定義從而完成內聯(替換)。


我們來看一個例子:

A.h :

class A
{
public:
 A(int a, int b) : a(a),b(b){}
 int max();

private:
 int a;
 int b;
};

A.cpp : 

#include "A.h"

inline int A::max()
{
 return a > b ? a : b;
}

Main.cpp : 

#include <iostream>
#include "A.h"
using namespace std;

inline int A::max()
{
 return a > b ? a : b;
}

int main()
{
 A a(3, 5);
 cout<<a.max()<<endl;
 return 0;
}

一切正常編譯,輸出結果:5

倘若你在Main.cpp中沒有定義max內聯函數,那麼會出現鏈接錯誤:

error LNK2001: unresolved external symbol "public: int __thiscall A::max(void)" (?max@A@@QAEHXZ)main.obj
找不到函數的定義,所以內聯函數可以在程序中定義不止一次,只要 inline 函數的定義在某個源文件中只出現一次,而且在所有源文件中,其定義必須是完全相同的就可以。

在頭文件中加入或修改 inline 函數時,使用了該頭文件的所有源文件都必須重新編譯。

內聯雖有它的好處,但是也要慎用,以下摘自《高質量程序設計指南——C++/C語言》:


而在Google C++編碼規範中則規定得更加明確和詳細:

內聯函數:

Tip: 只有當函數只有 10 行甚至更少時纔將其定義爲內聯函數.

定義: 當函數被聲明爲內聯函數之後, 編譯器會將其內聯展開, 而不是按通常的函數調用機制進行調用.
優點: 當函數體比較小的時候, 內聯該函數可以令目標代碼更加高效. 對於存取函數以及其它函數體比較短, 性能關鍵的函數, 鼓勵使用內聯.
缺點: 濫用內聯將導致程序變慢. 內聯可能使目標代碼量或增或減, 這取決於內聯函數的大小. 內聯非常短小的存取函數通常會減少代碼大小, 但內聯一個相當大的函數將戲劇性的增加代碼大小. 現代處理器由於更好的利用了指令緩存, 小巧的代碼往往執行更快。
結論: 一個較爲合理的經驗準則是, 不要內聯超過 10 行的函數. 謹慎對待析構函數, 析構函數往往比其表面看起來要更長, 因爲有隱含的成員和基類析構函數被調用!
另一個實用的經驗準則: 內聯那些包含循環或 switch 語句的函數常常是得不償失 (除非在大多數情況下, 這些循環或 switch 語句從不被執行).
有些函數即使聲明爲內聯的也不一定會被編譯器內聯, 這點很重要; 比如虛函數和遞歸函數就不會被正常內聯. 通常, 遞歸函數不應該聲明成內聯函數.(遞歸調用堆棧的展開並不像循環那麼簡單, 比如遞歸層數在編譯時可能是未知的, 大多數編譯器都不支持內聯遞歸函數). 虛函數內聯的主要原因則是想把它的函數體放在類定義內, 爲了圖個方便, 抑或是當作文檔描述其行爲, 比如精短的存取函數.

-inl.h文件:

Tip: 複雜的內聯函數的定義, 應放在後綴名爲 -inl.h 的頭文件中.

內聯函數的定義必須放在頭文件中, 編譯器才能在調用點內聯展開定義. 然而, 實現代碼理論上應該放在 .cc 文件中, 我們不希望 .h 文件中有太多實現代碼, 除非在可讀性和性能上有明顯優勢.

如果內聯函數的定義比較短小, 邏輯比較簡單, 實現代碼放在 .h 文件裏沒有任何問題. 比如, 存取函數的實現理所當然都應該放在類定義內. 出於編寫者和調用者的方便, 較複雜的內聯函數也可以放到 .h 文件中, 如果你覺得這樣會使頭文件顯得笨重, 也可以把它萃取到單獨的 -inl.h 中. 這樣把實現和類定義分離開來, 當需要時包含對應的 -inl.h 即可。







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章