1.為什么要引入虛擬繼承
虛擬繼承是多重繼承中特有的概念。虛擬基類是為解決多重繼承而出現的。如:類D繼承自類B1、B2,而類B1、B2都繼 承自類A,因此在類D中兩次出現類A中的變量和函數。為了節省內存空間,可以將B1、B2對A的繼承定義為虛擬繼承,而A就成了虛擬基類。實現的代碼如 下:
class A
class B1:public virtual A;
class B2:public virtual A;
class D:public B1,public B2;
虛擬繼承在一般的應用中很少用到,所以也往往被忽視,這也主要是因為在C++中,多重繼承是不推薦的,也並不常用,而一旦離開了多重繼承,虛擬繼承就完全失去了存在的必要因為這樣只會降低效率和占用更多的空間。
為什么需要虛繼承?
由於C++支持多重繼承,那么在這種情況下會出現重復的基類這種情況,也就是說可能出現將一個類兩次作為基類的可能性。比如像下面的情況
1 #include<iostream> 2 using std::cout; 3 using std::endl; 4 class Base 5 { 6 protected: 7 int value; 8 public: 9 Base() 10 { 11 cout<<"in Base"<<endl; 12 } 13 }; 14 class DerivedA:protected Base 15 { 16 public: 17 DerivedA() 18 { 19 cout<<"in DerivedA"<<endl; 20 } 21 }; 22 class DerivedB: protected Base 23 { 24 public: 25 DerivedB() 26 { 27 cout<<"in DerivedB"<<endl; 28 } 29 }; 30 class MyClass:DerivedA,DerivedB 31 { 32 public: 33 MyClass() 34 { 35 cout<<"in MyClass"<<value<<endl; 36 } 37 };
編譯時的錯誤如下
這 中情況下會造成在MyClass中訪問value時出現路徑不明確的編譯錯誤,要訪問數據,就需要顯示地加以限定。變成DerivedA::value或 者DerivedB::value,以消除歧義性。並且,通常情況下,像Base這樣的公共基類不應該表示為兩個分離的對象,而要解決這種問題就可以用虛 基類加以處理。如果使用虛繼承,編譯便正常了,類的結構示意圖便如下。
虛繼承的特點是,在任何派生類中的virtual基類總用同一個(共享)對象表示,正是如上圖所示。
2.引入虛繼承和直接繼承會有什么區別呢
由於有了間接性和共享性兩個特征,所以決定了虛繼承體系下的對象在訪問時必然會在時間和空間上與一般情況有較大不同。
2.1時間:在通過繼承類對象訪問虛基類對象中的成員(包括數據成員和函數成員)時,都必須通過某種間接引用來完成,這樣會增加引用尋址時間(就和虛函數一樣),其實就是調整this指針以指向虛基類對象,只不過這個調整是運行時間接完成的。
2.2空間:由於共享所以不必要在對象內存中保存多份虛基類子對象的拷貝,這樣較之 多繼承節省空間。虛擬繼承與普通繼承不同的是,虛擬繼承可以防止出現diamond繼承時,一個派生類中同時出現了兩個基類的子對象。也就是說,為了保證 這一點,在虛擬繼承情況下,基類子對象的布局是不同於普通繼承的。因此,它需要多出一個指向基類子對象的指針。
3.筆試,面試中常考的C++虛擬繼承的知識點
第一種情況: 第二種情況: 第三種情況 第四種情況:
class a class a class a class a
{ { { {
virtual void func(); virtual void func(); virtual void func(); virtual void func();
}; }; char x; char x;
class b:public virtual a class b :public a }; };
{ { class b:public virtual a class b:public a
virtual void foo(); virtual void foo(); { {
}; }; virtual void foo(); virtual void foo();
}; };
如果對這四種情況分別求sizeof(a), sizeof(b)。結果是什么樣的呢?下面是輸出結果:(在vc6.0中運行)
第一種:4,12
第二種:4,4
第三種:8,16
第四種:8,8
詳細分析可參考:http://blog.csdn.net/wangqiulin123456/article/details/8059536
想想這是為什么呢?
因為每個存在虛函數的類都要有一個4字節的指針指向自己的虛函數表,所以每種情況的類a所占的字節數應該是沒有什么問題 的,那么類b的字節數怎么算呢?看“第一種”和“第三種”情況采用的是虛繼承,那么這時候就要有這樣的一個指針vptr_b_a,這個指針叫虛類指針,也 是四個字節;還要包括類a的字節數,所以類b的字節數就求出來了。而“第二種”和“第四種”情況則不包括vptr_b_a這個指針,這回應該木有問題了 吧。
1 class a 2 { 3 virtual void func(); 4 }; 5 6 class b:public a 7 { 8 void foo(); 9 };
此時:sizeof(a) = 4 , sizeof(b) = 4
1 class a 2 { 3 virtual void func(); 4 }; 5 6 class b:public a 7 { 8 virtual void foo(); 9 };
奇怪的是,此時:sizeof(a) = 4 , sizeof(b) = 4。 盡管class b中在voif foo()前加了virtual,但結果卻相同。
1 class a 2 { 3 virtual void func(); 4 }; 5 6 class b:public virtual a 7 { 8 void foo(); 9 };
此時:sizeof(a) = 4 , sizeof(b) = 8
1 class a 2 { 3 void func(); 4 }; 5 6 class b:public a 7 { 8 virtual void foo(); 9 };
此時:sizeof(a) = 1 , sizeof(b) = 4
1 class a 2 { 3 void func(); 4 }; 5 6 class b:public a 7 { 8 void foo(); 9 };
此時:sizeof(a) = 1 , sizeof(b) = 1
如下例:

1 class A 2 { 3 }; 4 class A2 5 { 6 }; 7 class B : public A 8 { 9 }; 10 class C : public virtual B 11 { 12 }; 13 class D : public A , public A2 14 { 15 };
以上答案分別是1 , 1 , 4 , 1. 這說明:空類所占空間為1,單一繼承的空類空間也為1,多重繼承的空類空間還是1.但是虛繼承涉及到虛表(虛指針),所以sizeof(C)的大小為4
我相信經過上面的分析和對比,以后看到這類問題不會再疑惑,會有一種“水落石出”的感覺。
關於字節的求取更詳細的總結見:http://www.cnblogs.com/heyonggang/p/3253036.html
http://www.cnblogs.com/heyonggang/archive/2012/12/11/2812304.html
4.c++重載、覆蓋、隱藏的區別和執行方式
既然說到了繼承的問題,那么不妨討論一下經常提到的重載,覆蓋和隱藏
4.1成員函數被重載的特征
(1)相同的范圍(在同一個類中);
(2)函數名字相同;
(3)參數不同;
(4)virtual 關鍵字可有可無。
4.2“覆蓋”是指派生類函數覆蓋基類函數,特征是:
(1)不同的范圍(分別位於派生類與基類);
(2)函數名字相同;
(3)參數相同;
(4)基類函數必須有virtual 關鍵字。
4.3“隱藏”是指派生類的函數屏蔽了與其同名的基類函數,特征是:
(1)如果派生類的函數與基類的函數同名,但是參數不同,此時,不論有無virtual關鍵字,基類的函數將被隱藏(注意別與重載混淆)。
(2)如果派生類的函數與基類的函數同名,但是參數相同,但是基類函數沒有virtual 關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)。
小結:說白了就是如果派生類和基類的函數名和參數都相同,屬於覆蓋,這是可以理解的吧,完全一樣當然要覆蓋了;如果只是函數名相同,參數並不相同,則屬於隱藏。
4.4 三種情況怎么執行:
4.4.1 重載:看參數。
4.4.2 隱藏:用什么就調用什么。
4.4.3 覆蓋:調用派生類。
5.C++子類繼承父類后子類的大小
1 #include <iostream> 2 using namespace std; 3 class A 4 { 5 private: 6 int a; 7 }; 8 9 class B:public A 10 { 11 private: 12 int b; 13 }; 14 15 int main() 16 { 17 cout<<sizeof(A)<<endl; 18 cout<<sizeof(B)<<endl; 19 return 0; 20 }
剛開始我一想子類繼承父類不會繼承父類的私有變量,如此我認為結果為4,4(錯誤)。而事實上結果是4,8。也就是說子類把父類的私有變量也繼承下來了,但是卻無法訪問,對於我這種菜鳥來說一下子沒法轉個彎來,后來看看資料煥然大悟,子類雖然無法直接訪問父類的私有變量,但是子類繼承的父類的函數卻可以訪問,不然的話如果只繼承函數而不繼承變量,哪么父類的函數豈不成了無米之炊了。所以必須把父類的所有變量都繼承下來,這樣既能保護父類的變量也能使用父類的函數。
6.C++虛擬繼承的實際大小
輸出下面class的大小:
- class X{};
- class Y : public virtual X{};
- class Z : public virtual X{};
- class A : public Y, public Z{};
繼承關系如下圖:
這是可能大家就會覺得他們的大小都應該是0,因為他們中沒有任何一個有明顯的數據,只表示了繼承關系。但是至少也認為class x應該是0吧,他什么都沒有。結果卻讓你想不到,我在vs2010環境下測試的大小是:(不同編譯器可能這個大小是不一樣)
- cout<<"sizeof X: " <<sizeof X<<endl
- <<"sizeof Y: " <<sizeof Y<<endl
- <<"sizeof Z: " <<sizeof Z<<endl
- <<"sizeof A: " <<sizeof A<<endl;
很奇怪吧,為什么是這個結果呢。一個空的class事實上並不是空,它有一個隱藏的1 byte,這個是編譯器安插進去的char,這樣就可以保證定義的對象在內存中的大小是獨一無二的,這個地方你可以自己測試下,比如:
- X xa,xb;
- if (&xa == &xb)
- cout<<"is equal"<<endl;
- else
- cout<<"not equal"<<endl;
但是讓人搞不懂的是Y、Z的大小。主要大小受三個因素的影響:
- 語言本身所造成的額外負擔,當語言支持虛基類的時候,就導致一個額外的負擔,這個一般都是一個虛表指針。里面存儲的就是虛基類子對象的地址,就是偏移量。
- 編 譯器對於特殊情況所提供的優化處理,因為class X有1 byte的大小,這樣就出現在了class Y和class Z身上。這個主要視編譯器而定,比如某些存在這個1byte但是有些編譯器就將他忽略了(因為已經用虛指針了所以這個1byte就可以不用作為內存中的一 個定位)。
- Alignment的限制,就是所謂的對齊操作,比如你現在占用5bytes編譯器為了更有效率地在內存中存取就將其對齊為8byte。
下面說明在vs2010中的模型,因為有了虛指針后所以1byte就不用了,所以class Y和class Z的大小就是4bytes,如下圖:
現在你覺得class A的大小應該是多少呢?一個虛基類子對象只會在派生類中存在一份實體,不管他在繼承體系中出現多少次,所以公用一個1byte的classX實體,再加上 class Y和class Z這樣就有9bytes,如果有對齊的話就是12bytes但是vs2010中省略了那1byte所以就不存在對齊就直接是8bytes。謎底終於揭開 了!!!