當一個函數是內聯和虛函數時,會發生代碼替換或使用虛表調用嗎? 為了弄 清楚內聯和虛函數,讓我們將它們分開來考慮。通常,一個內聯函數是被展開的 。
class CFoo {
private:
int val;
public:
int GetVal() { return val; }
int SetVal(int v) { return val=v; }
};
這裡,如果使用下列代碼:
CFoo x;
x.SetVal(17);
int y = x.GetVal();
那麼編譯器產生的目標代碼將與下面的代碼段一樣:
CFoo x;
x.val = 17;
int y = x.val;
你當然不能這麼做,因為val是個私有變量。內聯函數的優點是不用函數調用 就能隱藏數據,僅此而已。
虛函數有多態性,意味著派生的類能實現相同的函數,但功能卻不同。假設 GetVal 被聲明為虛函數,並且你有第二個 以不同方法實現的類 CFoo2:
class CFoo2 : public CFoo {
public:
// virtual in base class too!
virtual int CFoo2::GetVal() {return someOtherVal;}
};
如果 pFoo是一個 CFoo 或 CFoo2 指針,那麼,無論 pFoo 指向哪個類 CFoo 或 CFoo2,成員函數 pFoo->GetVal 都能調用成功。
如果一個函數既是虛擬函數,又是內聯函數,會是什麼情況呢?記住,有兩 種方式建立內聯函數,
第一種是在函數定義中使用關鍵字 inline,如:
inline CFoo::GetVal() { return val; }
第二種是在類的聲明中編寫函數體 ,就象前面的 CFoo2::GetVal 一樣。所以如果將虛函數體包含在類的聲明中, 如:
class CFoo {
public:
virtual int GetVal() { return val; }
};
編譯器便認為這個函數 GetVal 是內聯的,同時也是虛擬 的。那麼,多態性和內聯特性如何同時工作呢?
編譯器遵循的第一個規則是無論發生什麼事情,多態性必須起作用。如果有 一個指向 CFoo 對象的指針,pFoo->GetVal 被保證去調用正確的函數。一般 情況下,這就是說函數 GetVal 將被實例化為非內聯函數,並有vtable(虛表) 入口指向它們。但這並不意味著這個函數不能被擴展!再看看下面的代碼:
CFoo x;
x.SetVal(17)
int y = x.GetVal()
編譯器知道x是 CFoo,而不是CFoo2,因為這個堆對象是被顯式聲明的。x肯定 不會是CFoo2。所以展開 SetVal/GetVal 內聯是安全的。如果要寫更多的復雜代 碼:
CFoo x;
CFoo* pfoo=&x;
pfoo->SetVal(17);
int y = pfoo->GetVal();
...
CFoo2 x2;
pfoo = &x2;
pfoo->SetVal(17); //etc.
編譯器知道 pfoo 第一次指向x, 第二次指向x2,所以展開虛擬函數也是安全的。
你還可以編寫更復雜的代碼,其中,pfoo 所指的對象類型總是透明的,但是 大多數編譯器不會做任何更多的分析。即使在前面的例子中,某些編譯器將會安 全運行,實例化並通過一個虛表來調用。實際上,編譯器總是忽略內聯需要並總 是使用虛表。唯一絕對的規則是代碼必須工作;也就是說,虛函數必須有多態行 為。
通常,無論是顯式還是隱式內聯,它只是一個提示而已,並非是必須的,就 象寄存器一樣。編譯器完全能拒絕展開一個非虛內聯函數,C++編譯器常常首先 會報錯:“內聯中斷-函數太大”。如果內聯函數調用自身,或者你 在某處傳遞其地址,編譯器必須產生一個正常(外聯?)函數。內聯函數在 DEBUG BUILDS中不被展開,可設置編譯選項來預防。
要想知道編譯器正在做什麼,唯一的方法是看它產生的代碼。對於微軟的編 譯器來說,你可以用-FA編譯選項產生匯編清單。你不必知道匯編程序如何做。 我鼓勵你完成這個實驗;這對於了解機器實際所做的事情機器有益,同時你可學 習許多匯編列表中的內容。
有關內聯函數的東西比你第一次接觸它時要復雜得多。有許多種情況強迫編 譯器產生正常函數:遞歸,獲取函數地址,太大的那些函數和虛函數。但是如果 編譯器決定實例化你的內聯函數,就要考慮把函數放在什麼地方?它進入哪個模 塊?
通常類在頭文件中聲明,所以如果某個cpp包含foo.h,並且編譯器決定實例 化CFoo::GetVal,則在cpp文件中將它實例化成一個靜態函數。如果十個模塊包 含foo.h,編譯器產生的虛函數拷貝就有十個。實際上,可以用虛表指向不同類 型的GetVal拷貝,從而是相同類型的對象只產生拷貝。一些鏈接器能巧妙地在鏈 接時排除冗余,但一般你是不能指望他來保證的。
我們得出的結論是:最好不要使用內聯虛函數,因為它們幾乎不會被展開, 即便你的函數只有一行,你最好還是將它與其它的類函數一起放在模塊(cpp文 件)中。當然,開發者常常將簡短的虛函數放在類聲明中-不是因為他們希望這 個函數被展開為內聯,而是因為這樣做更方便和可讀性更強。