虛擬繼承是C++語言中一個非常重要但是又比較生僻的存在,它的定義非常簡單,但是對于理解C++的繼承機制卻是非常有用的。筆者最近學習過程中發現對C++的虛擬繼承不是很明朗,故在這里對虛繼承做個小結。
首先說下遇到的問題吧。代碼如下(代碼來自于何海濤《程序員面試精選100題第32題)。意圖是要設計一個不能被繼承的類,類似java中的final。但是又可以生成棧對象,可以像一般的C++類一樣使用。
[cpp]?view plain
?copy ? #include?"stdafx.h"?? #include?<iostream>?? using?namespace?std;?? template?<class?T>?class?MakeFinal?? {?? ????friend?T;?? private:?? ????MakeFinal()?? ????{?? ????????cout<<"in?MakeFinal"<<endl;?? ????}?? ????~MakeFinal(){}?? };?? class?FinalClass:?virtual?public?MakeFinal<FinalClass>?? {?? public:?? ????FinalClass()?? ????{?? ????????cout<<"in?FinalClass"<<endl;?? ????}?? ????~FinalClass(){}?? };?? ?? class?Try:?public?FinalClass?? {?? public:?? ????Try()?? ????{?? ????????cout<<"in?Try"<<endl;?? ????}?? };??
這樣的確使得FinalClass不能被繼承了,原因在于類FinalClass是從類MakeFinal<Final>虛繼承過來的,在調用Try的構造函數的時候,會直接跳過FinalClass而直接調用MakeFinal<FinalClass>的構造函數。而Try不是MakeFinal<Final>的友元,所以這里就會出現編譯錯誤。但是如果把虛繼承改成一般的繼承,這里就沒什么問題了。筆者對這里的調用順序不是很明朗,為了對虛繼承有徹底的了解,故做個小結。將從下面幾個方向進行總結:1、為何要有虛繼承;2、虛繼承對于類的對象布局的影響;3、虛基類對構造函數的影響;
1、為什么需要虛繼承
由于C++支持多重繼承,那么在這種情況下會出現重復的基類這種情況,也就是說可能出現將一個類兩次作為基類的可能性。比如像下面的情況
[cpp]?view plain
?copy ? #include<iostream>?? using?std::cout;?? using?std::endl;?? class?Base?? {?? protected:?? ????int?value;?? public:?? ????Base()?? ????{?? ????????cout<<"in?Base"<<endl;?? ????}?? };?? class?DerivedA:protected?Base?? {?? public:?? ????DerivedA()?? ????{?? ????????cout<<"in?DerivedA"<<endl;?? ????}?? };?? class?DerivedB:?protected?Base?? {?? public:?? ????DerivedB()?? ????{?? ????????cout<<"in?DerivedB"<<endl;?? ????}?? };?? class?MyClass:DerivedA,DerivedB?? {?? public:?? ????MyClass()?? ????{?? ????????cout<<"in?MyClass"<<value<<endl;?? ????}?? };??
編譯時的錯誤如下
這中情況下會造成在MyClass中訪問value時出現路徑不明確的編譯錯誤,要訪問數據,就需要顯示地加以限定。變成DerivedA::value或者DerivedB::value,以消除歧義性。并且,通常情況下,像Base這樣的公共基類不應該表示為兩個分離的對象,而要解決這種問題就可以用虛基類加以處理。如果使用虛繼承,編譯便正常了,類的結構示意圖便如下。
虛繼承的特點是,在任何派生類中的virtual基類總用同一個(共享)對象表示,正是如上圖所示。
2、虛繼承對類的對象布局的影響
要理解多重繼承情況中重復基類時為什么會出現訪問路徑不明確的編譯錯誤,需要了解繼承中類對象在內存中的布局。在C++繼承中,子類會繼承父類的成員變量,因此在子類對象在內存中會包括來自父類的成員變量。實例代碼如下,輸出結果表明了每個對象在內存中所占的大小。
[cpp]?view plain
?copy ? #include<iostream>?? using?std::cout;?? using?std::endl;?? class?Base?? {?? protected:?? ????int?value;?? public:?? ????Base()?? ????{?? ?????????? ????}?? };?? class?DerivedA:protected??Base?? {?? protected:?? ????int?valueA;?? public:??? ????DerivedA()?? ????{?? ?????????? ????}?? };?? class?DerivedB:?protected??Base?? {?? protected:?? ????int?valueB;?? public:?? ????DerivedB()?? ????{?? ?????????? ????}?? };?? class?MyClass:DerivedA?? {?? private:?? ????int?my_value;?? public:?? ????MyClass()?? ????{?? ?????????? ????}?? };?? int?main()?? {?? ????Base?base_obj;?? ????DerivedA?derA_obj;?? ????MyClass?my_obj;?? ????cout<<"size?of?Base?object?"<<sizeof(base_obj)<<endl;?? ????cout<<"size?of?DerivedA?object?"<<sizeof(derA_obj)<<endl;?? ????cout<<"size?of?MyClass?object?"<<sizeof(my_obj)<<endl;?? }??
輸出結果如下
從類的定義結合這里的輸出便不難明白,在子類對象中是包含了父類數據的,即在C++繼承中,一個子類的object所表現出來的東西,是其自己的members加上其基類的member的總和。示意圖如下(這里只討論非靜態變量)
在單繼承的時候,訪問相關的數據成員時,只需要使用名字即可。但是,在多重繼承時,情況會變得復雜。因為重復基類中,在子類中變量名是相同的。這時,如果直接使用名字去訪問,便會出現歧義性。看下面的代碼以及對應的輸出
[cpp]?view plain
?copy ? #include<iostream>?? using?std::cout;?? using?std::endl;?? class?Base?? {?? protected:?? ????int?value;?? public:?? ????Base()?? ????{?? ?????????? ????}?? };?? class?DerivedA:protected??Base?? {?? protected:?? ????int?valueA;?? public:??? ????DerivedA()?? ????{?? ?????????? ????}?? };?? class?DerivedB:?protected??Base?? {?? protected:?? ????int?valueB;?? public:?? ????DerivedB()?? ????{?? ?????????? ????}?? };?? class?MyClass:DerivedA,DerivedB?? {?? private:?? ????int?my_value;?? public:?? ????MyClass()?? ????{?? ?????????? ????}?? };?? int?main()?? {?? ????Base?base_obj;?? ????DerivedA?derA_obj;?? ????MyClass?my_obj;?? ????cout<<"size?of?Base?object?"<<sizeof(base_obj)<<endl;?? ????cout<<"size?of?DerivedA?object?"<<sizeof(derA_obj)<<endl;?? ????cout<<"size?of?MyClass?object?"<<sizeof(my_obj)<<endl;?? }??
輸出如下
代碼的變化之處在于MyClass同時繼承了DerivedA和DerivedB。而my_obj在內存中的大小變成了20,比之前大了8.正好是增加了繼承至DerivedB中的數據部分的大小。上面情況中,my_obj在內存中的布局示意圖如下
從圖中可以看到,來自Base基類的數據成員value重復出現了兩次。這也正是為什么在MyClass中直接訪問value時會出現訪問不明確的問題了。
那么使用虛繼承后,對象的數據在內存中的布局又是什么樣子呢?按照預測,既然在my_obj中只有一份來自Base的value,那么大小是否就是16呢?
代碼及輸出如下
[cpp]?view plain
?copy ? #include<iostream>?? using?std::cout;?? using?std::endl;?? class?Base?? {?? protected:?? ????int?value;?? public:?? ????Base()?? ????{?? ?????????? ????}?? };?? class?DerivedA:protected??virtual?Base?? {?? protected:?? ????int?valueA;?? public:??? ????DerivedA()?? ????{?? ?????????? ????}?? };?? class?DerivedB:?protected?virtual?Base?? {?? protected:?? ????int?valueB;?? public:?? ????DerivedB()?? ????{?? ?????????? ????}?? };?? class?MyClass:DerivedA,DerivedB?? {?? private:?? ????int?my_value;?? public:?? ????MyClass()?? ????{?? ?????????? ????}?? };?? int?main()?? {?? ????Base?base_obj;?? ????DerivedA?derA_obj;?? ????DerivedB?derB_obj;?? ????MyClass?my_obj;?? ????cout<<"size?of?Base?object?"<<sizeof(base_obj)<<endl;?? ????cout<<"size?of?DerivedA?object?"<<sizeof(derA_obj)<<endl;?? ????cout<<"size?of?DerivedB?object?"<<sizeof(derB_obj)<<endl;?? ????cout<<"size?of?MyClass?object?"<<sizeof(my_obj)<<endl;?? };??
輸出結果如下
可以看到,DerivedA和DerivedB對象的大小變成了12,而MyClass對象的大小則變成了24.似乎大大超出了我們的預料。這其實是由于編譯器在其中插入了一些東西用來尋找這個共享的基類數據所用而造成的。(來自《深度探索C++對象模型》第3章 侯捷譯)這樣理解,Class如果內含一個或多個虛基類子對象,那么將被分割為兩部分:一個不變部分和一個共享部分。不變局部中的數據,不管后繼如何衍化,總是擁有固定的offset,所以這一部分數據可以直接存取。至于共享局部,所表現的就是虛基類子對象。根據編譯其的不同,會有不同的方式去得到這部分的數據,但總體來說都是需要有一個指向這部分共享數據的指針。
示意圖如下
當然實際編譯器使用的技術比這個要復雜,這里就不做詳細討論了。感興趣的朋友可以參見《深入探索C++對象模型》
3、虛繼承對構造函數的影響
對于構造函數的影響,借助于下面的原則可以理解(來自《深入理解C++對象模型》)
構造函數的調用可能內帶大量的隱藏碼,因為編譯器會對構造函數進行擴充,一般而言編譯器所作的擴充操作大約如下:
1、記錄在成員初始化列表中的數據成員的初始化操作會被放到構造函數本身中,按照數據成員聲明的順序
2、如果有一個數據成員沒有出現在初始化列表中,但是它有一個默認構造函數,那么這個默認構造函數會被調用
3、在那之前,如果有虛函數表,會調整虛函數表指針
4、在那之前,會對上一層基類的構造函數進行調用
5、在那之前,所有虛基類的構造函數必須被調用,按照聲明的繼承順序從左往右,從最深到最淺的順序
從上面的規則可以看出,對于虛基類的構造函數的調用是放在最前面的,并且需要子類對于虛基類的構造函數擁有訪問權限
從下面的示例代碼可以看出
[cpp]?view plain
?copy ? #include<iostream>?? using?std::cout;?? using?std::endl;?? class?Base?? {?? protected:?? ????int?value;?? public:?? ????Base()?? ????{?? ????????cout<<"in?Base"<<endl;?? ????}?? };?? class?DerivedA:protected??Base?? {?? protected:?? ????int?valueA;?? public:??? ????DerivedA()?? ????{?? ????????cout<<"in?DerivedA"<<endl;?? ????}?? };?? class?DerivedB?? {?? protected:?? ????int?valueB;?? public:?? ????DerivedB()?? ????{?? ????????cout<<"in?DerivedB"<<endl;?? ????}?? };?? class?TestClass?? {?? public:?? ????TestClass()?? ????{?? ????????cout<<"in?TestClass"<<endl;?? ????}?? };?? class?MyClass:DerivedA,virtual?DerivedB?? {?? private:?? ????int?my_value;?? ????TestClass?testData;?? public:?? ????MyClass()?? ????{?? ?????????? ????}?? };?? int?main()?? {?? ????? ? ? ? ? ? ? ? ?? ????MyClass?my_obj;?? }??
代碼運行后的效果如下所示
雖然在聲明繼承順序的時候DerivedA的順序是在DerivedB的前面的,但是由于DerivedB是虛擬繼承,所以對DerivedB的調用會在最前。但是如果將DerivedA繼承也改成虛繼承,那么調用順序就會發生變化。并且具體DerivedA的構造函數的調用與DerivedB的構造函數的調用順序還與是MyClass虛繼承DerivedA還是DerivedA虛繼承Base有關。還是用代碼來說明
MyClass虛繼承DerivedA的情況,由于DerivedA聲明在DerivedB前面,并且都是虛繼承,所以先調用DerivedA的構造函數。但DerivedA的父類是Base,所以具體的構造函數的調用順序是Base、DerivedA、DerivedB、TestClass。這種情況代碼如下
[cpp]?view plain
?copy ? #include<iostream>?? using?std::cout;?? using?std::endl;?? class?Base?? {?? protected:?? ????int?value;?? public:?? ????Base()?? ????{?? ????????cout<<"in?Base"<<endl;?? ????}?? };?? class?DerivedA:protected??Base?? {?? protected:?? ????int?valueA;?? public:??? ????DerivedA()?? ????{?? ????????cout<<"in?DerivedA"<<endl;?? ????}?? };?? class?DerivedB?? {?? protected:?? ????int?valueB;?? public:?? ????DerivedB()?? ????{?? ????????cout<<"in?DerivedB"<<endl;?? ????}?? };?? class?TestClass?? {?? public:?? ????TestClass()?? ????{?? ????????cout<<"in?TestClass"<<endl;?? ????}?? };?? class?MyClass:virtual?DerivedA,virtual?DerivedB?? {?? private:?? ????int?my_value;?? ????TestClass?testData;?? public:?? ????MyClass()?? ????{?? ?????????? ????}?? };?? int?main()?? {?? ????? ? ? ? ? ? ? ? ?? ????MyClass?my_obj;?? }??
輸出如下
但如果將虛繼承放在由DerivedA虛繼承Base,而MyClass非虛繼承DerivedA。那么按照從左往右,從深到淺的順序,調用順序應該是Base、DerivedB、DerivedA、TestClass
,代碼示例如下
[cpp]?view plain
?copy ? #include<iostream>?? using?std::cout;?? using?std::endl;?? class?Base?? {?? protected:?? ????int?value;?? public:?? ????Base()?? ????{?? ????????cout<<"in?Base"<<endl;?? ????}?? };?? class?DerivedA:protected?virtual??Base?? {?? protected:?? ????int?valueA;?? public:??? ????DerivedA()?? ????{?? ????????cout<<"in?DerivedA"<<endl;?? ????}?? };?? class?DerivedB?? {?? protected:?? ????int?valueB;?? public:?? ????DerivedB()?? ????{?? ????????cout<<"in?DerivedB"<<endl;?? ????}?? };?? class?TestClass?? {?? public:?? ????TestClass()?? ????{?? ????????cout<<"in?TestClass"<<endl;?? ????}?? };?? class?MyClass:?DerivedA,virtual?DerivedB?? {?? private:?? ????int?my_value;?? ????TestClass?testData;?? public:?? ????MyClass()?? ????{?? ?????????? ????}?? };?? int?main()?? {?? ????? ? ? ? ? ? ? ? ?? ????MyClass?my_obj;?? }??
輸出如下
這里還有一個問題是,左右順序和深淺順序該如何抉擇呢?答案是先左右,再深淺。比如將上面的代碼改為class MyClas: virutal DerivedB, DerivedA{...}
那么最后的調用順序就是DerivedB,Base, DerivedA,TestClass。讀者可以自己驗證。
總結
以上是生活随笔為你收集整理的C++虚继承(九) --- 构造函数调用顺序的实用之处的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。