# [9] 內聯函數
## FAQs in section [9]:
* [[9.1] 內聯函數有什么用?](#%5B9.1%5D)
* [[9.2] 有沒有個簡單的例子說明什么是順序集成(procedure integration)?](#%5B9.2%5D)
* [[9.3] 內聯函數能改善性能么?](#%5B9.3%5D)
* [[9.4] 內聯函數如何在安全和速度上取得折衷?](#%5B9.4%5D)
* [[9.5] 為什么我應該用內聯函數?而不是原來清晰的 #define 宏?](#%5B9.5%5D)
* [[9.6] 如何告訴編譯器使非成員函數成為內聯函數?](#%5B9.6%5D)
* [[9.7] 如何告訴編譯器使一個成員函數成為內聯函數?](#%5B9.7%5D)
* [[9.8] 有其它方法告訴編譯器使成員函數成為內聯嗎?](#%5B9.8%5D)
* [[9.9] 在定義于類外部的內聯函數中,以下哪種方法最好:是把inline關鍵字放在類內部的成員函數聲明前呢,還是放到類外部函數的定義前呢,還是兩個地方都寫?](#%5B9.8%5D)
## 9.1 內聯函數有什么用?
當編譯器內聯展開一個函數調用時,該函數的代碼會被插入到調用代碼流中(概念上類似于展開`#define`宏)。這能夠改善性能(當然還有很多其它因素),因為優化器能夠順序集成(procedurally integrate)被調用代碼,即將被調用代碼直接優化進調用代碼中。
有幾種方法將一個函數設定為內聯。其中一些需要使用`inline`關鍵字),還有一些則不需要。 不管你用何種方法設定函數為內聯,這只是個請求,而編譯器可以忽略它。編譯器可能會展開內聯函數調用,也可能不展開。(這看上去非常模糊,但不要為之沮 喪。這種靈活性其實有很大優點:這可以讓編譯器能夠區別對待很長的函數和短的函數,另外如果選擇了正確的編譯選項,還能使編譯器生成易于調試的代碼。)
## 9.2 有沒有個簡單的例子說明什么是順序集成(procedure integration)?
考慮下面對函數`g()`的調用:
```
?void?f()
?{
???int?x?=?/*...*/;
???int?y?=?/*...*/;
???int?z?=?/*...*/;
???...使用x,?y?和?z的代碼...
???g(x,?y,?z);
???...更多使用x,?y?和?z的代碼...
?}
```
假設一個典型的C++實現包含有一系列寄存器和一個棧,在調用`g()`之前,寄存器和參數會被寫入棧中,然后在`g()`內部的棧中會讀出參數值,然后在 `g()`返回到`f()`時又會將這些寄存器的值讀出來并恢復到寄存器中。但這里面有很多不必要的讀寫操作,尤其是當編譯器能夠用寄存器來保存`x`、`y`和`z`時。每 個變量都會寫兩次(做為寄存器和做為參數)并且讀兩次(在`g()`內部使用和返回到`f()`時恢復寄存器)。
```
?void?g(int?x,?int?y,?int?z)
?{
???...使用x、?y?和?z的代碼...
?}
```
如果編譯器能夠內聯展開對`g()`的調用,那么所有這些內存操作就都會消失了。不用再讀寫寄存器了,因為根本沒有函數調用。各個參數也不必再被讀寫了,因為優化器知道它們已經在寄存器里了。
當然你所能獲得的好處可能會變化,在本FAQ之外還有很多很多變數。但以上的例子能夠揭示出順序集成時所發生的事情。
## 9.3 內聯函數能改善性能么?
可能會,也可能不會。有時可以。也許可以。
答案沒那么簡單。內聯函數可能會使代碼速度更快,也可能使速度變慢。可能會使可執行文件變大,也可能變小。可能會導致系統性能下降,也可能避免性能下降。內聯函數可能(經常是)與速度完全無關。
**`內聯函數`可能會使代碼速度_更快_:**正如[上面](#%5B9.2%5D)所說,順序集成可能會移除很多不必要的指令,這可能會加快速度。
**`內聯函數`可能會使代碼速度_更慢_:**過多的內聯可能會使代碼膨脹,在使用分頁虛擬內存的系統上,這可能會導致性能下降。換句話說,如果可執行文件過大,系統可能會花費很多時間到磁盤上獲取下一塊代碼。
**`內聯函數`可能會_增加_可執行文件尺寸:**這就是上面所說的代碼膨脹。例如,假設系統有100個內聯函數,每個展開后有100字節,并且被調用了100次。這就會增加1MB的大小。增加這么1MB會導致問題嗎?誰知道呢,但很可能就是這1MB導致系統性能下降。
**`內聯函數`可能會_減少_可執行文件尺寸:**如果不內聯展開函數體,編譯器可能會要產生更多代碼來壓入/彈出寄存器內容和參數。對于很小的函數來說會是這樣。如果優化器能夠通過順序集成消除雕大量冗余代碼的話,那么對大函數也會起作用(也就是說,優化器能夠使大函數變小)。
**`內聯函數`可能會導致系統_性能下降_:**內聯可能會導致二進制可執行文件尺寸變大,由此導致系統性能下降。
**`內聯函數`可能會_避免_系統性能下降:**即使可執行文件尺寸變大,當前正在使用的物理內存數量(即需要同時留在內存中的頁面數量)卻仍然可能降低。當f()調用g()時,代碼經常分散在2個不同的頁面上。當編譯器將g()的代碼順序集成到f()后,代碼通常會放在一個頁面上。
**`內聯函數`可能會_降低_緩存的命中率:**內聯可能會導致內層循環跨越多行的內存緩存,這可能會導致內存和緩存頻繁交換,從而性能下降。
**`內聯函數`可能會_提高_緩存的命中率:**內聯通常能夠在二進制代碼中就近安排所用到的內容,這可能會減少用來存放內層循環代碼的緩存數量。最終這會使CPU密集型程序跑得更快。
**`內聯函數`可能與速度_無關_:**大多數系統不是CPU密集型的,而使I/O密集型的、數據庫密集型的或是網 絡密集型的。這表明系統的瓶頸存在于文件系統、數據庫或網絡。除非你的“CPU速度表”指示是100%,否則內聯函數可能不會使你的系統速度更快。(即使 是CPU密集型的系統,也只有在被用到瓶頸之處時,內聯才會有幫助。而瓶頸通常只存在于很少一部分代碼中。)
**沒有簡單定論:**你需要多試驗來找到最佳方案。_不要_指望依賴那些過分簡化的答案,比如“絕不要使用內聯函數”,或者“總是使用內聯函數”,再比如“當且僅當函數體少于N行代碼時使用內聯函數”。這種以一蓋全的準則寫下來很容易,但卻會產生不夠優化的結果。
譯注:這一小節中的性能下降主要是指系統因為頻繁交換內存頁而導致的性能下降。原文是thrashing。
## 9.4 內聯函數如何在安全和速度上取得折衷?
在?C?中,你可以通過在結構中設置一個?`void*`?來得到“封裝的結構”,在這種情況下,指向實際數據的?`void*`?指針對于結構的用戶來說是未知的。因此結構的用戶不知道如何解釋`void*`指針所指內容,但是存取函數可以將?`void*`?轉換成適當的隱含類型。這樣給出了封裝的一種形式。
不幸的是這樣做喪失了類型安全,并且即使僅僅是訪問結構體中的一個很不重要的字段也必須進行函數調用。(如果你允許直接存取結構的域,那么任何人都能直接存取該結構體了,因為他們必須了解如何解釋?`void*`?指針所指內容;這樣將使改變底層數據結構變的困難)。
雖然函數調用開銷是很小的,但它會被累積。C++類允許函數調用以內聯展開。這樣讓你在得到封裝的安全性時,同時得到直接存取的速度。此外,內聯函數的參數類型由編譯器檢查,這是對?C?的?`#define`?宏的一個改進。
## 9.5 為什么我應該用內聯函數?而不是原來清晰的 #define 宏?
因為`#define`宏有四宗罪:罪狀#1, 罪狀#2, 罪狀#3, 和 罪狀#4。有時雖然你會用它們,但它們仍然是邪惡的。
和?#define?宏不同的是,內聯函數總是對參數只精確地進行一次求值,從而避免了那聲名狼藉的宏錯誤。換句話說,調用內聯函數和調用正規函數是等價的,差別僅僅是更快:
```
//?返回 i?的絕對值的宏
?#define?unsafe(i)??\
?????????(?(i)?>=?0???(i)?:?-(i)?)
?//?返回?i 的絕對值的內聯函數
?inline
?int?safe(int?i)
?{
???return?i?>=?0???i?:?-i;
?}
?int?f();
?void?userCode(int?x)
?{
???int?ans;
???ans?=?unsafe(x++);???//?錯誤!x?被增加兩次
???ans?=?unsafe(f());???//?危險!f()被調用兩次
???ans?=?safe(x++);?????//?正確!?x?被增加一次
???ans?=?safe(f());?????//?正確!?f()?被調用一次
?}
```
和宏不同的,還有內聯函數的參數類型被檢查,并且被正確地進行必要的轉換。
宏是有害的;非萬不得已不要用。
## 9.6 如何告訴編譯器使非成員函數成為內聯函數?
聲明內聯函數看上去和普通函數非常相似:
```
void?f(int?i,?char?c);
```
當你定義一個內聯函數時,在函數定義前加上?`inline`?關鍵字,并且將定義放入頭文件:
```
?inline
?void?f(int?i,?char?c)
?{
???//?...
?}
```
注意:將函數的定義(`{`...`}`之間的部分)放在頭文件中是強制的,除非該函數僅僅被單個?`.cpp`?文件使用。尤其是,如果你將內聯函數的定義放在?`.cpp`?文件中并且在其他?`.cpp`文件中調用它,連接器將給出 “unresolved?external” 錯誤。
## 9.7 如何告訴編譯器使一個成員函數成為內聯函數?
聲明內聯成員函數看上去和普通成員函數非常類似:
```
?class?Fred?{
?public:
???void?f(int?i,?char?c);
?};
```
但是當你定義內聯成員函數時,在成員函數定義前加上?`inline`?關鍵字,并且將定義放入頭文件中:
```
?inline
?void?Fred::f(int?i,?char?c)
?{
???//?...
?}
```
通常將函數的定義(`{`...`}`之間的部分)放在頭文件中是強制的,除非函數只在一個`.cpp`文件中用到。特別 是,如果你將內聯函數的定義放在?`.cpp`?文件中并且在其他?`.cpp`?文件中調用它,連接器將給出 “unresolved?external”錯誤。
## 9.8 有其它方法告訴編譯器使成員函數成為內聯嗎?
有:在類體內定義成員函數:
```
?class?Fred?{
?public:
???void?f(int?i,?char?c)
?????{
???????//?...
?????}
?};
```
盡管這對于寫類的人來說很容易,但由于它將類是“什么”(what)和類“如何”(how)工作混在一起,給閱讀的人帶來了困難。我們通常更愿意[在類體外使用?`inline`?關鍵字](#%5B9.7%5D)定義成員函數來避免這種混合。這種感覺所基于的認識是:在一個面向重用的世界中,使用你的類的人有很多,而編寫它的人只有一個(你自己);因此你做任何事都應該照顧多數而不是少數。[下一條FAQ](#%5B9.9%5D)進一步應用了這個方法。
## 9.9 在定義于類外部的內聯函數中,以下哪種方法最好:是把`inline`關鍵字放在類內部的成員函數聲明前呢,還是放到類外部函數的定義前呢,還是兩個地方都寫?
最佳實踐是:僅放在類外部函數的定義前。
```
?class?Foo?{
?public:
???void?method();??//←?best?practice:?don't?put?the?inline?keyword?here...
?};
?inline?void?Foo::method()??//←?best?practice:?put?the?inline?keyword?here
?{?...?}
```
這里是基本的想法:
* 類的`public`部分是你描述類的_可見語義_的地方,包含公有成員函數、友元函數和任何其它暴露給外部的內容。不要提供在調用者代碼中看不到的細節。
* 類的其它部分,包括非公有部分、成員定義和友元函數聲明等等,這些純粹是實現細節。如果還沒有在類的公有部分描述,那么不要提供相關可見語義。
從一種實際的觀點來看,這種隔離能夠使用戶更輕松和更安全。假設`Chuck`只是想“用”你的類。因為你讀了本FAQ并使用了上述隔離辦 法,`Chuck`能夠在類的公有部分找到所有需要的內容,而不必看任何不需要的內容。他能夠更加輕松,因為只需要看一個地方。同時也會更安全,因為他純潔的 思想不必受到實現細節的干擾。
回到內聯上來:一個函數是否內聯只是實現細節,不會改變函數調用的可見語義(即含義)。因此`inline`關鍵字應該和函數定義放在一起,而不是在類的`public`聲明區。
注意:大部分人使用“聲明”和“定義”來區分以上所述的兩個位置。例如,人們會說“我應該把`inline`關鍵字放到聲明那里還是放在定義那里?”但 這種說法不太嚴密,可能會有人因此笑話你。笑話你的人可能只是不自信而又裝腔作勢的可憐蟲,他們無法在其生命中取得一些成就。然而,你還是可以學會使用正 確的術語來避免被笑話。其實,每個定義同時也是聲明。也就是說,如果把這兩者當作是互斥的,那么就好像是在問鋼和金屬哪個更重。當你把“定義”說成是“聲 明”的對立面時,幾乎所有人都都明白你的意思。只有最糟糕的癡迷于技術的小人物才會因此嘲笑你。但至少你知道如何正確使用術語。
- C++ FAQ Lite
- [1] 復制許可
- [2] 在線站點分發本文檔
- [3] C++-FAQ-Book 與 C++-FAQ-Lite
- [6] 綜述
- [7] 類和對象
- [8] 引用
- [9] 內聯函數
- [10] 構造函數
- [11] 析構函數
- [12] 賦值算符
- [13] 運算符重載
- [14] 友元
- [15] 通過 <iostream> 和 <cstdio>輸入/輸出
- [16] 自由存儲(Freestore)管理
- [17] 異常和錯誤處理
- [18] const正確性
- [19] 繼承 — 基礎
- [20] 繼承 — 虛函數
- [21] 繼承 — 適當的繼承和可置換性
- [22] 繼承 — 抽象基類(ABCs)
- [23] 繼承 — 你所不知道的
- [24] 繼承 — 私有繼承和保護繼承
- [27] 編碼規范
- [28] 學習OO/C++
- [31] 引用與值的語義
- [32] 如何混合C和C++編程
- [33] 成員函數指針
- [35] 模板 ?
- [36] 序列化與反序列化
- [37] 類庫