首先在談到菱形虛擬繼承之前先說明一下菱形繼承:
菱形繼承是多繼承的一種特殊情況(如下,畫渣勿噴):
圖中 B C 兩個類都繼承了A類,而 B C 又都被 D類繼承
按照繼承的定義,派生類當中都包含了基類,而這時虛擬繼承這種情況就會產生問題
<a>數據冗餘
首先是按照虛擬繼承的這種方法,D類當中就包含了兩份A類數據(分別來自B類和C類)
<b>二義性
其次是,當我們通過D類去訪問A類中的數據時,就會產生二義性,編譯器不知道是要去訪問D類中B中包含的A類數據還是C中包含的A類數據。
這時就要引入菱形虛擬繼承來了,但其實二義性的問題即使不使用菱形虛擬繼承也是可以解決的,相反數據冗餘不行:
class A{
public:
int aa = 0;
};
class B : public A{
};
class C : public A{};
class D : public B, public C{};
int main(){
D d1;
//d1.aa = 10;按照以上定義,如果運行這一句就會出現aa不明確的錯誤
//若是像以下這樣顯示指定則沒有問題
d1.C::aa = 10;
d1.B::aa = 20;
return 0;
}
但是既然數據冗餘的問題解決不了,我們還是需要使用菱形虛擬繼承,即如下代碼:
class A{
public:
int aa = 0;
};
class B : virtual public A{};
class C : virtual public A{};
class D : public B, public C{};
int main(){
D d1;
d1.aa = 10; //此時執行這一句就不會有問題
return 0;
}
當aa複製成功後的監視窗口如下
(D的對象內存模型↑)
沒有采用菱形虛擬繼承前,從內存模型中看可以很明顯看出存在兩份A的數據,自然會有數據冗餘和二義性的問題
但是菱形虛擬繼承從內存模型中看,B和C在虛擬繼承A時所存儲的不是A的數據,而是A的地址偏移量的地址,這兩個指針叫虛基表指針,兩個表叫虛基表。虛基表中存的就是偏移量。通過偏移量便可以找到下面的A。
流程就是:當訪問到A中數據時,首先通過偏移量地址找到相對當前位置A的地址偏移量,然後通過地址偏移量再找到A的數據,這樣就解決了兩大問題。
這裏有兩個需要關注的點:
一是虛擬繼承主要就是用來解決菱形繼承問題,其他地方不可亂用。
二是一般不建議設計出多繼承,一定不要設計出菱形繼承,否則程序在複雜性和性能上會有較大問題(多繼承可以認爲是C++的缺陷之一,所以後來很多語言都捨棄了多繼承,比方說JAVA)
====================================================================
現在再來說多繼承指針漂移的問題
先來看一段代碼
class A{
public:
int aa = 0;
};
class B{
public:
int bb = 0;
};
class C :public A, public B{
public:
int cc = 0;
};
int main(){
C p;
A* a1 = &p;
B* b1 = &p;
C* c1 = &p;
cout << a1 << ' ' << b1 << ' ' << c1 << endl;
if (b1 == c1){
cout << "b1 = c1" << endl;
}
return 0;
}
不瞭解這個問題的人,肯定會覺得a1,b1,c1三個指針的值應該是一樣的,但事實上↓
這個問題就很玄乎了,賦的是同樣的值,爲什麼b1和c1的值會不同呢?但是爲什麼下面的if判斷卻又判斷他兩相同呢?
首先是第一個問題,導致地址不同的原因是 在C進行繼承時有一個先後順序(按照代碼的編寫順序),他會先繼承A類,再繼承B類,最後再實現自己的部分,這就導致了,a1指向了C對象中A的位置,b1指向了C對象中B的位置,從而導致了地址不同。
但爲什麼 if 判斷會通過呢?這是第二個問題,當判斷 ‘==’ 時,會分析兩個地址是不是指向了同一個實例對象,如果是,就會做隱式的類型轉換 ,然後再判等(雖然地址不同但指向的是同一個實例對象)。這就導致這樣的結果。
多繼承指針漂移和虛擬繼承都是C++繼承部分需要知道的點,最好還是加以記憶。