天天看點

C++ 内聯函數inline一、引入inline關鍵字的原因2.inline使用限制

一、引入inline關鍵字的原因

在c/c++中,為了解決一些頻繁調用的小函數大量消耗棧空間(棧記憶體)的問題,特别的引入了inline修飾符,表示為内聯函數。

棧空間就是指放置程式的局部資料(也就是函數内資料)的記憶體空間。

在系統下,棧空間是有限的,假如頻繁大量的使用就會造成因棧空間不足而導緻程式出錯的問題,如,函數的死循環遞歸調用的最終結果就是導緻棧記憶體空間枯竭。

下面我們來看一個例子:

#include <stdio.h>
//函數定義為inline即:内聯函數
inline char* dbtest(int a) {
    return (i % 2 > 0) ? "奇" : "偶";
} 

int main()
{
   int i = 0;
   for (i=1; i < 100; i++) {
       printf("i:%d    奇偶性:%s /n", i, dbtest(i));    
   }
}
           

上面的例子就是标準的内聯函數的用法,使用inline修飾帶來的好處我們表面看不出來,其實,在内部的工作就是在每個for循環的内部任何調用dbtest(i)的地方都換成了(i%2>0)?”奇”:”偶”,這樣就避免了頻繁調用函數對棧記憶體重複開辟所帶來的消耗。

二、内聯函數的程式設計風格

1、關鍵字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 與函數定義體放在一起  
{  
} 
           

是以說,inline 是一種“用于實作的關鍵字”,而不是一種“用于聲明的關鍵字”。一般地,使用者可以閱讀函數的聲明,但是看不到函數的定義。盡管在大多數教科書中内聯函數的聲明、定義體前面都加了inline 關鍵字,但我認為inline 不應該出現在函數的聲明中。這個細節雖然不會影響函數的功能,但是展現了高品質C++/C 程式設計風格的一個基本原則:聲明與定義不可混為一談,使用者沒有必要、也不應該知道函數是否需要内聯。

2.inline使用限制

inline的使用是有所限制的,inline隻适合涵數體内代碼簡單的涵數使用,不能包含複雜的結構控制語句例如while、switch,并且不能内聯函數本身不能是直接遞歸函數(即,自己内部還調用自己的函數)。

限制:a.函數不可有循環;b.不能有switch語句; c.不能含有遞歸和靜态變量

3. 内聯函數應該在頭檔案中定義

定義在類聲明之中的成員函數将自動地成為内聯函數,例如:

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)" ([email protected]@@QAEHXZ)main.obj

找不到函數的定義,是以内聯函數可以在程式中定義不止一次,隻要 inline 函數的定義在某個源檔案中隻出現一次,而且在所有源檔案中,其定義必須是完全相同的就可以。

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

三、慎用内聯

       内聯能提高函數的執行效率,為什麼不把所有的函數都定義成内聯函數?如果所有的函數都是内聯函數,還用得着“内聯”這個關鍵字嗎?

       内聯是以代碼膨脹(複制)為代價,僅僅省去了函數調用的開銷,進而提高函數的執行效率。如果執行函數體内代碼的時間,相比于函數調用的開銷較大,那麼效率的收

獲會很少。另一方面,每一處内聯函數的調用都要複制代碼,将使程式的總代碼量增大,消耗更多的記憶體空間。

以下情況不宜使用内聯:

(1)如果函數體内的代碼比較長,使用内聯将導緻記憶體消耗代價較高。

(2)如果函數體内出現循環,那麼執行函數體内代碼的時間要比函數調用的開銷大。

一個好的編譯器将會根據函數的定義體,自動地取消不值得的内聯(這進一步說明了inline 不應該出現在函數的聲明中)。

内聯函數:

Tip: 隻有當函數隻有 10 行甚至更少時才将其定義為内聯函數.

定義: 當函數被聲明為内聯函數之後, 編譯器會将其内聯展開, 而不是按通常的函數調用機制進行調用. 

優點: 當函數體比較小的時候, 内聯該函數可以令目标代碼更加高效. 對于存取函數以及其它函數體比較短, 性能關鍵的函數, 鼓勵使用内聯. 

缺點: 濫用内聯将導緻程式變慢. 内聯可能使目标代碼量或增或減, 這取決于内聯函數的大小. 内聯非常短小的存取函數通常會減少代碼大小, 但内聯一個相當大的函數将戲劇性的增加代碼大小. 現代處理器由于更好的利用了指令緩存, 小巧的代碼往往執行更快。 

結論: 一個較為合理的經驗準則是, 不要内聯超過 10 行的函數. 謹慎對待析構函數, 析構函數往往比其表面看起來要更長, 因為有隐含的成員和基類析構函數被調用! 

另一個實用的經驗準則: 内聯那些包含循環或 switch 語句的函數常常是得不償失 (除非在大多數情況下, 這些循環或 switch 語句從不被執行). 

有些函數即使聲明為内聯的也不一定會被編譯器内聯, 這點很重要; 比如虛函數和遞歸函數就不會被正常内聯. 通常, 遞歸函數不應該聲明成内聯函數.(遞歸調用堆棧的展開并不像循環那麼簡單, 比如遞歸層數在編譯時可能是未知的, 大多數編譯器都不支援内聯遞歸函數). 虛函數内聯的主要原因則是想把它的函數體放在類定義内, 為了圖個友善, 抑或是當作文檔描述其行為, 比如精短的存取函數.

-inl.h檔案:

Tip: 複雜的内聯函數的定義, 應放在字尾名為 -inl.h 的頭檔案中.

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

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

題目

  • 關于c++的inline關鍵字,以下說法正确的是() 

    正确答案: D 你的答案: A (錯誤)

A. 使用inline關鍵字的函數會被編譯器在調用處展開

B. 頭檔案中可以包含inline函數的聲明

C. 可以在同一個項目的不同源檔案内定義函數名相同但實作不同的inline函數

D. 定義在Class聲明内的成員函數預設是inline函數

E. 優先使用Class聲明内定義的inline函數

F. 優先使用Class實作的内inline函數的實作

解析:

A 如果隻聲明含有inline關鍵字,就沒有内聯的效果。 内聯函數的定義必須放在頭檔案中, 編譯器才能在調用點内聯展開定義. 有些函數即使聲明為内聯的也不一定會被編譯器内聯, 這點很重要; 比如虛函數和遞歸函數就不會被正常内聯. 通常, 遞歸函數不應該聲明成内聯函數.

B 内聯函數應該在頭檔案中定義,這一點不同于其他函數。編譯器在調用點内聯展開函數的代碼時,必須能夠找到 inline 函數的定義才能将調用函數替換為函數代碼,而對于在頭檔案中僅有函數聲明是不夠的。

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

對于由兩個檔案compute.C和draw.C構成的程式來說,程式員不能定義這樣的min()函數,它在compute.C中指一件事情,而在draw.C中指另外一件事情。如果兩個定義不相同,程式将會有未定義的行為:

為保證不會發生這樣的事情,建議把inline函數的定義放到頭檔案中。在每個調用該inline函數的檔案中包含該頭檔案。這種方法保證對每個inline函數隻有一個定義,且程式員無需複制代碼,并且不可能在程式的生命期中引起無意的不比對的事情。

D 正确。 定義在類聲明之中的成員函數将自動地成為内聯函數,例如: 

class A { public: void Foo(int x, int y) { … } // 自動地成為内聯函數 }

E、F 在每個調用該inline函數的檔案中包含該頭檔案。這種方法保證對每個inline函數隻有一個定義,且程式員無需複制代碼,并且不可能在程式的生命期中引起無意的不比對的事情。最好隻有一個定義!

繼續閱讀