內聯(inline)函式與虛擬函式(virtual)的討論

cumtli發表於2019-04-16

函式的inline屬性是在編譯時確定的,然而,virtual的性質是在執行時確定的,這兩個不能同時存在,只能有一個選擇,檔案中的inline關鍵字只是對編譯器的建議,編譯器是否採納是編譯器的事情。

  • 行內函數是個靜態行為,而虛擬函式是個動態行為,他們之間是有矛盾的。
  • 我們之所以能看到一些象行內函數的虛擬函式,是因為某個函式是否是行內函數不是由我們說的算,而是由編譯器決定的。我們只能向編譯器建議,某個函式可以是行內函數(inline關鍵字),但是編譯器有自己的判斷法則。所以可能出現這樣的情況:
  1. 我們用inline宣告的函式卻沒有inline
  2. 我們沒有用inline宣告的函式卻是inline
  3. 對於inline函式,編譯器仍然將它編譯成一個有地址的函式

所以,情況比較複雜,從high-level來看的話很難判斷函式是否是inline的,如果從low-level來看的話就比較清晰,非行內函數遵從函式呼叫機制,在彙編中用call來呼叫。行內函數則沒有這些。

inline函式表示該函式是內聯的,它建議編譯程式在呼叫該函式的地方直接將函式的程式碼展開來插入caller的程式碼中.這個只是一種指示至於會不會被內聯,編譯程式還會根據被宣告為inline的函式的內部結構如:是否包含迴圈,複雜的函式呼叫等等來選擇是否inline。

虛擬函式肯定不會被內聯這一點毋庸置疑,因為虛擬函式只有到了Runtime才能被識別到底是哪一個被呼叫,而內聯是編譯期就會將程式碼展開並安插這個明顯不是一回事。
inline有兩種表現方式一種就是以inline在實現檔案中(.cpp)指出這被稱為顯示內聯,另外一種就如你所說類的宣告和定義放入同一個檔案這稱為隱式內聯,但是還是如前面所說inline只是一個提示符至於會不會內聯還是由編譯程式說了算。

ps:怎麼檢測一個標記為inline的函式是否被編譯器當做行內函數來處理呢。這裡提供一個方法:使用nm來檢視呼叫行內函數的目標檔案,如果找到了inline函式的符號,那麼說明沒有被當做行內函數,如果沒有找到的話,則說明編譯器把它當做了行內函數來處理。因為如果標記為inline的函式如果被當做了行內函數,編譯器應當直接使用程式碼替換掉呼叫標記,所以不應當看到有行內函數的符號。

分割
構造方法和析構方法不應該是inline,為啥?

C++對於“物件被建立和被銷燬時發生什麼事”做個保證。比如:構造過程出現異常,C++保證構造好的那一部分自動銷燬。這就意味著,為了滿足這種保證,C++在構造方法中,增加了一些程式碼。如果將構造方法宣告為inline,意味著文字替換,這就妨礙編譯器新增一些程式碼。

如下:

class base { 
public: 
    ... 
private: 
    std::string bm1, bm2; 
};
class Derived : public Base { 
public: 
    Derived(){}  //Derived 建構函式是空的 是嗎? 
    ... 
private: 
    std::string dm1, dm2, dm3; 
};
複製程式碼

這個建構函式看起來是inlining的絕佳候選人,因為他根本不含任何程式碼,但是:
c++對於“物件被建立和被銷燬時發生什麼事”做了各式各樣的保證。編譯器為稍早說的那個表面上看起來是空的Derived建構函式所產生的程式碼,相當於以下所列:

Derived::Derived() 
{ 
   Base::Base(); 
    try{dm1.std::string::string();} 
    catch(...){ 
        Base::~Base(); 
        throw; 
    } 
    try{dm2.std::string::string();} 
    catch(...){ 
        dm1.std::string::~string(); 
        Base::~Base(); 
        throw; 
    } 
}
複製程式碼

這段程式碼並不能代表編譯器真正製造出來的程式碼,但是不論編譯器在其內所做的異常處理多麼精緻複雜,Derived建構函式至少一定會陸續呼叫其成員變數和base class兩者的建構函式,而那些呼叫(它們自身也可能被inlined)會影響編譯器是否對此空白函式inlining。

相關文章