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++中,多重繼承是不推薦的,也並不常用,而一旦離開了多重繼承,虛擬繼承就完全失去了存在的必要因爲這樣只會降低效率和佔用更多的空間。
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
想想這是爲什麼呢?
因爲每個存在虛函數的類都要有一個4字節的指針指向自己的虛函數表,所以每種情況的類a所佔的字節數應該是沒有什麼問題的,那麼類b的字節數怎麼算呢?看“第一種”和“第三種”情況採用的是虛繼承,那麼這時候就要有這樣的一個指針vptr_b_a,這個指針叫虛類指針,也是四個字節;還要包括類a的字節數,所以類b的字節數就求出來了。而“第二種”和“第四種”情況則不包括vptr_b_a這個指針,這回應該木有問題了吧。
對虛繼承層次的對象的內存佈局,在不同編譯器實現有所區別。
首先,說說GCC的編譯器.
它實現比較簡單,不管是否虛繼承,GCC都是將虛表指針在整個繼承關係中共享的,不共享的是指向虛基類的指針。
class A {
int a;
virtual ~A(){}
};
class B:virtual public A{
virtual void myfunB(){}
};
class C:virtual public A{
virtual void myfunC(){}
};
class D:public B,public C{
virtual void myfunD(){}
};
以上代碼中 sizeof(A)=8,sizeof(B)=12,sizeof(C)=12,sizeof(D)=16.
解釋:A中int+虛表指針。B,C中由於是虛繼承因此大小爲A+指向虛基類的指針,B,C雖然加入了自己的虛函數,但是虛表指針是和基類共享的,因此不會有自己的虛表指針。D由於B,C都是虛繼承,因此D只包含一個A的副本,於是D大小就等於A+B中的指向虛基類的指針+C中的指向虛基類的指針。
如果B,C不是虛繼承,而是普通繼承的話,那麼A,B,C的大小都是8(沒有指向虛基類的指針了),而D由於不是虛繼承,因此包含兩個A副本,大小爲16. 注意此時雖然D的大小和虛繼承一樣,但是內存佈局卻不同。
然後,來看看VC的編譯器
vc對虛表指針的處理比GCC複雜,它根據是否爲虛繼承來判斷是否在繼承關係中共享虛表指針,而對指向虛基類的指針和GCC一樣是不共享,當然也不可能共享。
代碼同上。
運行結果將會是sizeof(A)=8,sizeof(B)=16,sizeof(C)=16,sizeof(D)=24.
解釋:A中依然是int+虛表指針。B,C中由於是虛繼承因此虛表指針不共享,由於B,C加入了自己的虛函數,所以B,C分別自己維護一個虛表指針,它指向自己的虛函數。(注意:只有子類有新的虛函數時,編譯器纔會在子類中添加虛表指針)因此B,C大小爲A+自己的虛表指針+指向虛基類的指針。D由於B,C都是虛繼承,因此D只包含一個A的副本,同時D是從B,C普通繼承的,而不是虛繼承的,因此沒有自己的虛表指針。於是D大小就等於A+B的虛表指針+C的虛表指針+B中的指向虛基類的指針+C中的指向虛基類的指針。
同樣,如果去掉虛繼承,結果將和GCC結果一樣,A,B,C都是8,D爲16,原因就是VC的編譯器對於非虛繼承,父類和子類是共享虛表指針的。