1. 用內聯取代巨集程式碼
C++ 語言支援函式內聯,其目的是為了提高函式的執行效率(速度)。
在 C 程式中,可以用巨集程式碼提高執行效率。巨集程式碼本身不是函式,但使用起來象函 數。 前處理器用複製巨集程式碼的方式代替函式呼叫, 省去了引數壓棧、 生成組合語言的 CALL 呼叫、返回引數、執行 return 等過程,從而提高了速度。使用巨集程式碼最大的缺點是容 易出錯 ,前處理器在複製巨集程式碼時常常產生意想不到的邊際效應。
例如
#define MAX(a, b) (a) > (b) ? (a) : (b)
語句
result = MAX(i, j) + 2 ;
將被前處理器解釋為
result = (i) > (j) ? (i) : (j) + 2 ;
由於運算子‘ +’比運算子‘ :’的優先順序高,所以上述語句並不等價於期望的
result = ( (i) > (j) ? (i) : (j) ) + 2 ;
如果把巨集程式碼改寫為
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
則可以解決由優先順序引起的錯誤。但是即使使用修改後的巨集程式碼也不是萬無一失的,例 如語句
result = MAX(i++, j);
將被前處理器解釋為
result = (i++) > (j) ? (i++) : (j);
對於 C++ 而言,使用巨集程式碼還有另一種缺點:無法操作類的私有資料成員 ,也就是說巨集程式碼基本是針對公共或全域性操作的。
讓我們看看 C++ 的“函式內聯”是如何工作的。對於任何行內函數,編譯器在符號 表裡放入函式的宣告(包括名字、引數型別、返回值型別) 。如果編譯器沒有發現內聯 函式存在錯誤,那麼該函式的程式碼也被放入符號表裡。在呼叫一個行內函數時,編譯器 首先檢查呼叫是否正確(進行型別安全檢查,或者進行自動型別轉換,當然對所有的函 數都一樣) 。如果正確,行內函數的程式碼就會直接替換函式呼叫,於是省去了函式呼叫 的開銷。這個過程與預處理有顯著的不同,因為前處理器不能進行型別安全檢查 ,或者 進行自動型別轉換。假如行內函數是成員函式,物件的地址( this)會被放在合適的地 方,這也是前處理器辦不到的。
C++ 語言的函式內聯機制既具備巨集程式碼的效率,又增加了安全性,而且可以自由操 作類的資料成員。所以在 C++ 程式中,應該用行內函數取代所有巨集程式碼, “斷言 assert” 恐怕是唯一的例外。 assert 是僅在 Debug 版本起作用的巨集,它用於檢查“不應該”發生 的情況。為了不在程式的 Debug 版本和 Release 版本引起差別, assert 不應該產生任何 副作用。 如果 assert 是函式, 由於函式呼叫會引起記憶體、 程式碼的變動, 那麼將導致 Debug 版本與 Release 版本存在差異。 所以 assert 不是函式, 而是巨集。
2. 行內函數的程式設計風格
關鍵字 inline 必須與函式定義 體放在一起才能使函式成為內聯 ,僅將 inline 放在 函式宣告前面不起任何作用 。如下風格的函式 Foo 不能成為行內函數:
1 2 3 4 5 6 7 8 9 10 11 |
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 程式設計風格 的一個基本原則:宣告與定義不可混為一談,使用者沒有必要、也不應該知道函式是否需 要內聯。
定義在類宣告之中的成員函式將自動地成為行內函數,例如
1 2 3 4 5 |
class A { public: void Foo(int x, int y) { … } // 自動地成為行內函數 } |
將成員函式的定義體放在類宣告之中雖然能帶來書寫上的方便,但不是一種良好的程式設計 風格,上例應該改成:
1 2 3 4 5 6 7 8 9 10 11 |
// 標頭檔案 class A { public: void Foo(int x, int y); } // 定義檔案 inline void A::Foo(int x, int y) { … } |
3. 慎用內聯
內聯能提高函式的執行效率,為什麼不把所有的函式都定義成行內函數?
如果所有的函式都是行內函數,還用得著“內聯”這個關鍵字嗎?
內聯是以程式碼膨脹 (複製)為代價,僅僅省去了函式呼叫的開銷,從而提高函式的 執行效率。如果執行函式體內程式碼的時間,相比於函式呼叫的開銷較大,那麼效率的收 獲會很少。另一方面,每一處行內函數的呼叫都要複製程式碼,將使程式的總程式碼量增大,
消耗更多的記憶體空間。以下情況不宜使用內聯:
( 1)如果函式體內的程式碼比較長,使用內聯將導致記憶體消耗代價較高。
( 2)如果函式體內出現迴圈,那麼執行函式體內程式碼的時間要比函式呼叫的開銷大。
類的建構函式和解構函式容易讓人誤解成使用內聯更有效。要當心建構函式和析構 函式可能會隱藏一些行為,如“偷偷地”執行了基類或成員物件的建構函式和解構函式。 所以不要隨便地將建構函式和解構函式的定義體放在類宣告中 。
一個好的編譯器將會根據函式的定義體,自動地取消不值得的內聯(這進一步說明 了 inline 不應該出現在函式的宣告中) 。
注:
參考《高質量C++程式設計指南》。