新聞中心
類的成員函數(shù)可以分為內聯(lián)函數(shù)和外聯(lián)函數(shù)。內聯(lián)函數(shù)是指那些定義在類體內的成員函數(shù),即該函數(shù)的函數(shù)體放在類體內。而說明在類體內,定義在類體外的成員函數(shù)叫外聯(lián)函數(shù)。外聯(lián)函數(shù)的函數(shù)體在類的實現(xiàn)部分。

內聯(lián)函數(shù)在調用時不是像一般的函數(shù)那樣要轉去執(zhí)行被調用函數(shù)的函數(shù)體,執(zhí)行完成后再轉回調用函數(shù)中,執(zhí)行其后語句,而是在調用函數(shù)處用內聯(lián)函數(shù)體的代碼來替換,這樣將會節(jié)省調用開銷,提高運行速度。
內聯(lián)函數(shù)與前面講過的帶參數(shù)的宏定義進行一下比較,它們的代碼效率是一樣的,但是內聯(lián)函數(shù)要優(yōu)于宏定義,因為內聯(lián)函數(shù)遵循函數(shù)的類型和作用域規(guī)則,它與一般函數(shù)更相近,在一些編譯器中,一旦關上內聯(lián)擴展,將與一般函數(shù)一樣進行調用,調試比較方便。
外聯(lián)函數(shù)變成內聯(lián)函數(shù)的方法很簡單,只要在函數(shù)頭前面加上關鍵字inline就可以了。
- #include
- using namespace std;
- class A
- {
- public:
- A(int x, int y) //內聯(lián)函數(shù)
- {
- X=x;Y=y;
- }
- int a() //內聯(lián)函數(shù)
- {
- return X;
- }
- int b() //內聯(lián)函數(shù)
- {
- return Y;
- }
- int c();
- int d();
- private:
- int X,Y;
- };
- //inline定義內聯(lián)函數(shù)
- inline int A::c()
- {
- return a()+b();
- }
- inline int A::d()
- {
- return c();
- }
- void main()
- {
- A m(3,5);
- int I=m.d();
- cout<<"d()return:"<
- }
輸出結果:
d()return:8
說明:類A中,直接定義了3個內聯(lián)函數(shù),又使用inline定義了2個內聯(lián)函數(shù)。
引入內聯(lián)函數(shù)的意義
函數(shù)是一種更高級的抽象。它的引入使得編程者只關心函數(shù)的功能和使用方法,而不必關心函數(shù)功能的具體實現(xiàn);函數(shù)的引入可以減少程序的目標代碼,實現(xiàn)程序代碼和數(shù)據(jù)的共享。但是,函數(shù)調用也會帶來降低效率的問題,因為調用函數(shù)實際上將程序執(zhí)行順序轉移到函數(shù)所存放在內存中某個地址,將函數(shù)的程序內容執(zhí)行完后,再返回到轉去執(zhí)行該函數(shù)前的地方。這種轉移操作要求在轉去前要保護現(xiàn)場并記憶執(zhí)行的地址,轉回后先要恢復現(xiàn)場,并按原來保存地址繼續(xù)執(zhí)行。
因此,函數(shù)調用要有一定的時間和空間方面的開銷,于是將影響其效率。特別是對于一些函數(shù)體代碼不是很大,但又頻繁地被調用的函數(shù)來講,解決其效率問題更為重要。引入內聯(lián)函數(shù)實際上就是為了解決這一問題。
在程序編譯時,編譯器將程序中出現(xiàn)的內聯(lián)函數(shù)的調用表達式用內聯(lián)函數(shù)的函數(shù)體來進行替換。顯然,這種做法不會產生轉去轉回的問題,但是由于在編譯時將函數(shù)體中的代碼被替代到程序中,因此會增加目標程序代碼量,進而增加空間開銷,而在時間代銷上不象函數(shù)調用時那么大,可見它是以目標代碼的增加為代價來換取時間的節(jié)省。
在程序中,調用其函數(shù)時,該函數(shù)在編譯時被替代,而不是像一般函數(shù)那樣是在運行時被調用。
使用內聯(lián)函數(shù)應注意的事項
內聯(lián)函數(shù)具有一般函數(shù)的特性,它與一般函數(shù)所不同之處只在于函數(shù)調用的處理。一般函數(shù)進行調用時,要將程序執(zhí)行權轉到被調用函數(shù)中,然后再返回到調用它的函數(shù)中;而內聯(lián)函數(shù)在調用時,是將調用表達式用內聯(lián)函數(shù)體來替換。在使用內聯(lián)函數(shù)時,應注意如下幾點:
1.在內聯(lián)函數(shù)內不允許用循環(huán)語句和開關語句。
如果內聯(lián)函數(shù)有這些語句,則編譯將該函數(shù)視同普通函數(shù)那樣產生函數(shù)調用代碼,遞歸函數(shù)(自己調用自己的函數(shù))是不能被用來做內聯(lián)函數(shù)的。內聯(lián)函數(shù)只適合于只有1~5行的小函數(shù)。對一個含有許多語句的大函數(shù),函數(shù)調用和返回的開銷相對來說微不足道,所以也沒有必要用內聯(lián)函數(shù)實現(xiàn)。
2.內聯(lián)函數(shù)的定義必須出現(xiàn)在內聯(lián)函數(shù)***次被調用之前。
3.本欄目講到的類結構中所有在類說明內部定義的函數(shù)是內聯(lián)函數(shù)。
#p#
深入探究內聯(lián)函數(shù)
內聯(lián)函數(shù)——多么振奮人心的一項發(fā)明!它們看上去與函數(shù)很相像,它們擁有與函數(shù)類似的行為,它們要比宏(參見第 2 條)好用的多,同時你在調用它們時帶來的開銷比一般函數(shù)小得多??芍^“內聯(lián)在手,別無他求?!?/p>
你得到的遠遠比你想象的要多,因為節(jié)約函數(shù)調用的開銷僅僅是冰山一角。編譯器優(yōu)化通常是針對那些沒有函數(shù)調用的代碼,因此當你編寫內聯(lián)函數(shù)時,編譯器就會針對函數(shù)體的上下文進行優(yōu)化工作。然而大多數(shù)編譯器都不會針對“外聯(lián)”函數(shù)調用進行優(yōu)化。
然而,在你的編程生涯中,“沒有免費的午餐”這句生活哲言同樣奏效,內聯(lián)函數(shù)不會幸免。內聯(lián)函數(shù)背后蘊含的理念是:用代碼本體來取代每次函數(shù)調用,這樣做很可能會是目標代碼的體積增大不少,這一點并不是非要統(tǒng)計學博士才能看得清。對于內存空間有限的機器而言,過分熱衷于使用內聯(lián)則會造成函數(shù)占用過多的空間。即使在虛擬內存中,那些冗余的內聯(lián)代碼也會帶來不少無謂的分頁,從而使緩存讀取命中率降低,最終帶來性能的犧牲。
另一方面,如果一個內聯(lián)函數(shù)體非常的短,那么為函數(shù)體所生成代碼的體積就會比為函數(shù)調用生成的代碼小一些。此時,內聯(lián)函數(shù)才真正做到了減小目標代碼和提高緩存讀取命中率的目的。
我們要時刻保持清醒, Inline 是對編譯器的一次請求,而不是一條命令。這種請求可以顯式提出也可以隱式提出。隱式請求的途徑就是:在類定義的內部定義函數(shù):
- class Person {
- public:
- ...
- int age() const { return theAge; }
- // 隱式內聯(lián)請求 : 年齡 age 在類定義中做出定義
- ...
- private:
- int theAge;
- };
這樣的函數(shù)通常是成員函數(shù),但是類中定義的函數(shù)也可以是友元(參見第 46 條),如果函數(shù)是友元,那么也應隱式將它們定義為內聯(lián)函數(shù)。
顯式聲明內聯(lián)函數(shù)的方法為:在函數(shù)定義之前添加 inline 關鍵字。比如說,下面是標準 max 模板(來自
- template
// 顯式內聯(lián)請求: - inline const T& std::max(const T& a, const T& b)
- { return a < b ? b : a; } // 在 std::max 的前邊添加 ”inline”
max 是一個模板這一事實,讓我們不免得出這樣的推論:內聯(lián)函數(shù)和模 板都應該在頭文件中定義。這就使一些程序員做出“函數(shù)模板必須為內聯(lián)函數(shù)”的論斷。這一結論不僅不合法,而且也存在潛在的害處,所以這里我們還是要大略的了解一下。
由于大多數(shù)構建環(huán)境都是在編譯過程中進行內聯(lián),因此內聯(lián)函數(shù)一般情況下都應該定義在頭文件中。編譯器必須首先了解函數(shù)的大致情況,以便于用所調用函數(shù)體來代替這次函數(shù)調用。(一些構建環(huán)境在連接過程中進行內聯(lián),還有個別基于 .NET 通用語言基礎結構( CLI )的托管環(huán)境甚至是在運行時進行內聯(lián)。這樣的環(huán)境僅僅屬于例外,而不是守則。在大多數(shù) C++ 程序中,內聯(lián)是一個編譯時行為。)
模板通常保存在頭文件中,但是編譯器還需要了解模板的大致情形,以便于在用到時進行正確的實例化。(然而,這并不是一成不變的。一些構建環(huán)境在連接時進行模板實例化。但是編譯時實例化才是更通用的方式。)
模板實例化相對于內聯(lián)是獨立的。如果你正在編寫一個模板,而你又確信由這個模板所實例化出的所有函數(shù)都應該是內聯(lián)的,那么這個模板就應該添 加 inline 關鍵字;這也就是上文中 std::max 實 現(xiàn)的做法。但是如果你正在編寫的模板并不需要實例化內聯(lián)函數(shù),那么就不需要聲明內聯(lián)模板(無論是顯式還是隱式)。
內聯(lián)也是有開銷的,不假思索就引入內聯(lián)的開銷的做法并不明智。我們已經(jīng)介紹過了內聯(lián)是如何使代碼膨脹起來的(對于模板的作者而言,還應該做更周密的考慮——參見第 44 條),但是內聯(lián)還會帶來其他的開銷,這就是下文中我們將要討論的問題。
inline 是對編譯器的一次請求,但編譯器可能會忽略它。在我們的討論開 始之前,我們首先要弄清這一點。大多數(shù)編譯器如果認為當前的函數(shù)過于復雜(比如包括循環(huán)或遞歸的函數(shù)),或者這個函數(shù)是虛函數(shù)(即使是最平常的虛函數(shù)調用),就會拒絕將其內聯(lián)。后一個結論很好理解。因為 virtual 意味著“等到運行時再指出要調用哪個程序,”而 inline 意味著“在執(zhí)行程序之前,使用要調用的函數(shù)來代替這次調用?!比绻幾g器不知道要調用哪個函數(shù),那么它們拒絕內聯(lián)函數(shù)體的做法就無可厚非了。
綜上所述,我們得出下面的結論:一個給定的函數(shù)是否得到內聯(lián),取決于你正在使用的構建環(huán)境——主要是編譯器。幸運的是,大多數(shù)編譯器擁有診斷機制,如果編譯器在內聯(lián)函數(shù)時失敗了,那么它們將會做出警告。
有些時候,即使編譯器認為某個函數(shù)非常適合進行內聯(lián),可是還是會為它提供一個函數(shù)體。舉例說,如果你的程序要取得某個內聯(lián)函數(shù)的地址,那么編譯器必須用典型的方法為其創(chuàng)建一個外聯(lián)的函數(shù)體。那么編譯器又怎樣讓一個指針去指向一個不存在的函數(shù)呢?再加上編譯器一般不會通過對函數(shù)指針的調用進行內聯(lián)這一事實,更能肯定這一結論:對于一個內聯(lián)函數(shù)的調用是否應該得到內聯(lián),取決于這一調用是如何進行的:
- inline void f() {...} // 假設編譯器樂意于將 f 的調用進行內聯(lián)
- void (*pf)() = f; // pf 指向 f
- ...
- f(); // 此調用將被內聯(lián),因為這是一次“正?!钡恼{用
- pf(); // 此調用很可能不會被內聯(lián),因為它是通過一個函數(shù)指針進行的
即使你從未使用函數(shù)指針,未得到內聯(lián)的函數(shù)依然“陰魂不散”,這是因為需求函數(shù)指針的不僅僅是程序員。比如,編譯器在為對象的數(shù)組進行構造或析構時,也會生成構造函數(shù)和析構函數(shù)的外聯(lián)副本,從而使它們可以得到這些函數(shù)的指針以便使用。
實際上,為構造函數(shù)和析構函數(shù)進行內聯(lián)通常不是一個好的選擇,這兩者甚至不如一些隨意挑選的“選手”。請看下面示例 中 Derived 類的構 造函數(shù):
- class Base {
- public:
- ...
- private:
- std::string bm1, bm2; // 基類成員1和2
- };
- class Derived: public Base {
- public:
- Derived() {} // 派生類的構造函數(shù)為空 — 還有別的可能 ?
- ...
- private:
- std::string dm1, dm2, dm3;// 派生類成員 1–3
- };
乍看上去,將這個構造函數(shù)進行內聯(lián)再適合不過了,因為它不包含任何代碼。其實你的眼睛欺騙了你。
C++ 對于在創(chuàng)建和銷毀對象的過程中發(fā)生的事件進行了多方面的保證。比如,當你使用 new 時,你動態(tài)創(chuàng)建的對象的構造函數(shù)就會自動將其初始化;當你使用 delete 時,將調用相關的析構函數(shù)。當你創(chuàng)建一個對象時。每個基類和該對象中的每個數(shù)據(jù)成員將自動得到構造,在銷毀這個對象時,針對兩者的析構過程將會自動進行。如果在對象的構造過程中有異常拋出,那么對象中已經(jīng)得到構造的部分將統(tǒng)統(tǒng)被自動銷毀。
在所有這些場景中, C++ 告訴你什么一定會發(fā)生,但它沒有說明如何發(fā)生。這一點取決于編譯器的實現(xiàn)者,但是必須要清楚的一點是,這些事情并不是自發(fā)的。你必須要在程序中添加一些代碼來實現(xiàn)它們。這些代碼一定存在于某處,它們由編譯器代勞,用于在編譯過程中插入你的程序中。一些時候它們就存在于構造函數(shù)和析構函數(shù)中,所以,對于上文中 Derived 的空構造函數(shù),我們可以將具體實現(xiàn)中生成的代碼等價看作:
- Derived::Derived() // Derived 空構造函數(shù)的抽象實現(xiàn)
- {
- Base::Base(); // 初始化 Base 部分
- try { dm1.std::string::string(); } // 嘗試構造 dm1
- catch (...) { // 如果拋出異常 ,
- Base::~Base(); // 銷毀基類部分 ,
- throw; // 并且傳播該異常
- }
- try { dm2.std::string::string(); } // 嘗試構造 dm2
- catch(...) { // 如果拋出異常 ,
- dm1.std::string::~string(); // 銷毀 dm1,
- Base::~Base(); // 銷毀基類部分 ,
- throw; // 并且傳播該異常
- }
- try { dm3.std::string::string(); } // 嘗試構造 dm3
- catch(...) { // 如果拋出異常 ,
- dm2.std::string::~string(); // 銷毀 dm2,
- dm1.std::string::~string(); // 銷毀 dm1,
- Base::~Base(); // 銷毀基類部分 ,
- throw; // 并且傳播該異常
- }
- }
這段代碼并不能完全真實反映出編譯器所做的事情,因為真實的編譯器采用的做法更加復雜。然而,上面的代碼可以較為精確地反映出 Derived 的“空”構造函數(shù)必須要提供的內容。無論編譯器處理異常的實現(xiàn)方式多么復雜, Derived 的構造函數(shù)必須至少為其數(shù)據(jù)成員和基類調用構造函數(shù),這些調用(可能就是內聯(lián)的)會使 Derived 顯得不那么適合進行內聯(lián)。
這 一推理過程對于 Base 的構造函數(shù)同樣適用,因此如果將 Base 內聯(lián),所有添加進其中的代碼同樣也會添加進 Derived 的構造函數(shù)中(通過 Derived 構造函數(shù)調用 Base 構造函數(shù)的過程)。同時,如果 string 的構造函數(shù)恰巧被內聯(lián)了,那么 Derived 的構造函數(shù)將為其復制出五份副本,分別對應 Derived 對象中包含的五個字符串(兩個繼承而來,另外三個系對象本身包括)。
現(xiàn)在,“Derived 的構造函數(shù)是否應該內聯(lián)不是一個純機械化問題”就很容易理解了。對于 Derived 的析構函數(shù)也一樣,你必須親自關注 Derived 的構造函數(shù)初始化的對象是否全部恰當?shù)牡玫戒N毀,這一點機器無法代替。
庫設計者必須估算出將函數(shù)內聯(lián)所帶來的影響,因為你根本無法為庫中客戶端程序員可見的內聯(lián)函數(shù)提供底層的升級。換句話說,如果 f 是庫中的一個內聯(lián)函數(shù),那么庫的客戶端程序員就會將 f 的函數(shù)體編譯進他們的程序中。隨后,如果一個庫實現(xiàn)者修改了 f 的內容,那么所有曾經(jīng)使用過 f 的客戶端程序員必須要重新編譯他們的代碼。這一點是我們所不希望看到的。
另一個角度講,如果 f 不是內聯(lián)函數(shù),那么修改 f 只需要客戶端程序員重新連接一下就可以了。這樣要比重新編譯減少很多繁雜的工作,并且,如果庫中需要使用的函數(shù)是動態(tài)鏈接的,那么它對于客戶端程序員就是完全透明的。
我們的目標是開發(fā)優(yōu)質的程序,因此要將這些重要問題牢記在心。但是以編寫代碼實際操作的角度來說,這一個事實將淹沒一切:大多數(shù)調試人員面對內聯(lián)函數(shù)時會遇到麻煩。這并不會令人意外,因為你無法為一個尚不存在的函數(shù)設定一個跟蹤點。一些構建環(huán)境試圖支持內聯(lián)函數(shù)的調試,但是幾乎都失敗了,大多數(shù)環(huán)境都是在調試過程中直接禁止內聯(lián)。
對于“哪個函數(shù)應該聲明為 inline 而哪些不應該”這一問題,我們可以由上文中引出一個邏輯上的策略。起初,不要內聯(lián)任何內容,或者僅挑選出那些不得不內聯(lián)的函數(shù)(參見第 46 條)或者那些確實是很細小的程序(比如本節(jié)開篇處出現(xiàn)的 Person::age )進行內聯(lián)。謹慎引入內聯(lián),你就為調試工作提供了方便,但是你仍然要為內聯(lián)擺正位置:它屬于手工的優(yōu)化操作。
不要忘記 80-20 經(jīng)驗決定主義原則:一個典型的程序將花去 80% 的時間僅僅運行 20% 的代碼。這是一個非常重要的原則,因為它時時刻刻提醒我們,軟件開發(fā)者的目標是:找出你的代碼中 20% 的這部分進行優(yōu)化,從而從整體上提高程序的性能。你可以花費很長的時間進行內聯(lián)、修改函數(shù)等等,但如果你沒有鎖定正確的目標,那么你做再多的努力也是徒勞。
結論:
僅僅對小型的、調用頻率高的程序進行內聯(lián)。這將簡化你的調試操作,為底層更新提供方便,降低潛在的代碼膨脹發(fā)生的可能,并且可以讓程序獲得更高的速度。不要將模板聲明為 inline 的,因為它們一般在頭文件中出現(xiàn)。
希望對你有幫助。
標題名稱:詳細介紹內聯(lián)函數(shù)的使用
URL地址:http://www.dlmjj.cn/article/djdciej.html


咨詢
建站咨詢
