inline用法詳解

劍西樓發表於2017-02-15

(一)inline函式(摘自C++ Primer的第三版)

      在函式宣告或定義中函式返回型別前加上關鍵字inline即把min()指定為內聯。

      inline int min(int first, int secend) {/****/};

        inline函式對編譯器而言必須是可見的,以便它能夠在呼叫點內展開該函式。與非inline函式不同的是,inline函式必須在呼叫該函式的每個文字檔案中定義。當然,對於同一程式的不同檔案,如果inline函式出現的話,其定義必須相同。對於由兩個檔案compute.C和draw.C構成的程式來說,程式設計師不能定義這樣的min()函式,它在compute.C中指一件事情,而在draw.C中指另外一件事情。如果兩個定義不相同,程式將會有未定義的行為.

        為保證不會發生這樣的事情,建議把inline函式的定義放到標頭檔案中。在每個呼叫該inline函式的檔案中包含該標頭檔案。這種方法保證對每個inline函式只有一個定義,且程式設計師無需複製程式碼,並且不可能在程式的生命期中引起無意的不匹配的事情。

(二)行內函數的程式設計風格(摘自高質量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 與函式定義體放在一起
{
}
所以說,inline 是一種“用於實現的關鍵字”,而不是一種“用於宣告的關鍵字”。一般地,使用者可以閱讀函式的宣告,但是看不到函式的定義。儘管在大多數教科書中行內函數的宣告、定義體前面都加了inline 關鍵字,但我認為inline 不應該出現在函式的宣告中。這個細節雖然不會影響函式的功能,但是體現了高質量C++/C 程式設計風格的一個基本原則:宣告與定義不可混為一談,使用者沒有必要、也不應該知道函式是否需要內聯。


定義在類宣告之中的成員函式將自動地成為行內函數,例如
class A
{
public:
void Foo(int x, int y) {  } // 自動地成為行內函數
}
將成員函式的定義體放在類宣告之中雖然能帶來書寫上的方便,但不是一種良好的程式設計
風格,上例應該改成:
// 標頭檔案
class A
{
public:
void Foo(int x, int y);
}
// 定義檔案
inline void A::Foo(int x, int y)
{
}

慎用內聯
內聯能提高函式的執行效率,為什麼不把所有的函式都定義成行內函數?
如果所有的函式都是行內函數,還用得著“內聯”這個關鍵字嗎?
內聯是以程式碼膨脹(複製)為代價,僅僅省去了函式呼叫的開銷,從而提高函式的
執行效率。如果執行函式體內程式碼的時間,相比於函式呼叫的開銷較大,那麼效率的收
獲會很少。另一方面,每一處行內函數的呼叫都要複製程式碼,將使程式的總程式碼量增大,
消耗更多的記憶體空間。以下情況不宜使用內聯:
(1)如果函式體內的程式碼比較長,使用內聯將導致記憶體消耗代價較高。
(2)如果函式體內出現迴圈,那麼執行函式體內程式碼的時間要比函式呼叫的開銷大。
類的建構函式和解構函式容易讓人誤解成使用內聯更有效。要當心建構函式和析構
函式可能會隱藏一些行為,如“偷偷地”執行了基類或成員物件的建構函式和解構函式。
所以不要隨便地將建構函式和解構函式的定義體放在類宣告中。
一個好的編譯器將會根據函式的定義體,自動地取消不值得的內聯(這進一步說明
了inline 不應該出現在函式的宣告中)。

C++ 語言支援函式內聯,其目的是為了提高函式的執行效率(速度)。
在C程式中,可以用巨集程式碼提高執行效率。巨集程式碼本身不是函式,但使用起來象函式。
前處理器用複製巨集程式碼的方式代替函式呼叫,省去了引數壓棧、生成組合語言的CALL呼叫、 
返回引數、執行return等過程,從而提高了速度。 

使用巨集程式碼最大的缺點是容易出錯,前處理器在複製巨集程式碼時常常產生意想不到的邊際效應。 

對於C++ 而言,使用巨集程式碼還有另一種缺點:無法操作類的私有資料成員。 

讓我們看看C++ 的"函式內聯"是如何工作的。 
對於任何行內函數,編譯器在符號表裡放入函式的宣告(包括名字、引數型別、返回值型別)。 
如果編譯器沒有發現行內函數存在錯誤,那麼該函式的程式碼也被放入符號表裡。 
在呼叫一個行內函數時,編譯器首先檢查呼叫是否正確 
(進行型別安全檢查,或者進行自動型別轉換,當然對所有的函式都一樣)。 
如果正確,行內函數的程式碼就會直接替換函式呼叫,於是省去了函式呼叫的開銷。

這個過程與預處理有顯著的不同,因為前處理器不能進行型別安全檢查,或者進行自動型別轉換。 
假如行內函數是成員函式,物件的地址(this)會被放在合適的地方,這也是前處理器辦不到的。 

C++ 語言的函式內聯機制既具備巨集程式碼的效率,又增加了安全性,而且可以自由操作類的資料成員。 
所以在C++ 程式中,應該用行內函數取代所有巨集程式碼,"斷言assert"恐怕是唯一的例外。 
assert是僅在Debug版本起作用的巨集,它用於檢查"不應該"發生的情況。 
為了不在程式的Debug版本和Release版本引起差別,assert不應該產生任何副作用。 
如果assert是函式,由於函式呼叫會引起記憶體、程式碼的變動,那麼將導致Debug版本與Release版本存在差異。 
所以assert不是函式,而是巨集。