鑽石繼承和虛繼承
在C++中,類是允許多繼承的,多繼承大大的提高了程式碼的複用、減少程式碼冗餘、大大的提高了類的表現力,使得類更貼近現實中的事物,使用起來更為靈活,更面向物件。
但由於這靈活的語法,使得C++使用起來比別的程式語言更為複雜,不過凡事有利必有弊,這裡就不去探討其中的利弊,還是把注意力放到使用繼承時候需要注意的地方。
鑽石繼承
什麼是鑽石繼承?
A
/ \
X Y
\ /
Z
鑽石繼承是多繼承的一種情況如上圖中:類A中派生出類X 和類Y ,類X和類Y派生出類Z,那麼類A稱為公共基類,類Z稱為匯合子類。
那麼我現在要編寫一個類Z,當例項一個Z物件的時候,該物件要包含A X Y的例項各一份。
那麼普通的多繼承方式能不能達到要求?
#include <stdio.h> #include <iostream> using namespace std; class A { public: A(int data) : m_data(data) { cout << "A構造 : " << this << endl; } protected: int m_data; }; class X : public A { public: X(int data) : A(data) { cout << "X構造 : " << this << endl; } // 獲得繼承於類A中m_data的值 int getData(void) const { return m_data; } }; class Y : public A { public: Y(int data) : A(data) { cout << "Y構造 : " << this << endl; } // 修改繼承於類A中m_data的值 void setData(int data) { m_data = data; } }; class Z : public X, public Y { public: Z(int data) : X(data), Y(data) { cout << "Z構造 : " << this << endl; } }; int main(void) { Z z(0); z.setData(100); //使用類Y的函式修改類A的資料 cout << "m_data = " << z.getData() << endl; // 通過類X的函式訪問類A的資料 return 0; }
編譯後的執行結果
首先發現了公共基類 A中的m_data並沒有成功,而且從列印資訊中發現類A例項化了2次,而且打印出來的地址分別和X和Y的地址一樣。這說明了Z的例項中存在了2份A的例項,分別存在於例項Y和例項X中。
在z.getData中,是通過例項X提供的函式訪問了例項X中的例項A。
在z.setData中,是通過例項Y提供的函式修改了例項Y中的例項A。
這說明了Z的例項通過不同的路徑訪問例項A,得到了不一樣的資料,這樣並沒有達到設計中的要求,而且使用起來也很不人性化。簡直是差評,太糟糕了。
解決方法: 使用虛繼承
為了令Z的例項中只擁有一份A的例項,可以採取虛繼承來解決鑽石繼承帶來的問題。
/*
通過虛繼承解決鑽石繼承帶來的問題
*/
#include <stdio.h>
#include <iostream>
using namespace std;
class A
{
public:
A(int data) : m_data(data)
{
cout << "A構造 : " << this << endl;
}
protected:
int m_data;
};
class X : virtual public A
{
public:
X(int data) : A(data)
{
cout << "X構造 : " << this << endl;
}
int getData(void) const
{
return m_data;
}
};
class Y : virtual public A
{
public:
Y(int data) : A(data)
{
cout << "Y構造 : " << this << endl;
}
void setData(int data)
{
m_data = data;
}
};
class Z : public X, public Y
{
public:
// 注意,採用虛繼承的話,必須在匯聚子類的建構函式中顯示的初始化公共基類,否則公共基類會呼叫預設構造
Z(int data) : X(data), Y(data), A(data)
{
cout << "Z構造 : " << this << endl;
}
};
int main(void)
{
Z z(0);
z.setData(100);
cout << "m_data = " << z.getData() << endl;
return 0;
}
執行結果:
通過虛繼承問題就能解決了,類A只構造了一次,所以在類Z的例項中只存在一份例項A。
虛指標 和 虛表
通過虛繼承方式被繼承的基類稱為虛基類,通過虛繼承派生的子類,會擁有一個虛指標,該指標指向一個虛表,虛表中記錄的該類的各種資訊,例如例項中與虛基類例項的偏移量,和虛擬函式與普通函式的入口地址。虛指標和虛表在C++實現多型的實現中起到重要的作用。
在程式碼中,A是Z的虛基類子物件,X和Y相對於Z是中間基類子物件。
Z的例項化過程如下(腦補的,如有錯誤請指出)
編譯期間,發現X和Y是採用了虛繼承,所以為其增加了一個虛指標和一個虛表,程式執行的時候先建立A,然後建立X,根據X的地址和A的地址,算出偏移量並存放到虛表中,Y與X一樣,最後建立Z。
記憶體模型:虛指標->虛基類表->虛基類子物件相對於中間基類子物件的偏移量。
這樣在執行期間就能通過虛指標訪問虛表,在從虛表中取得偏移量,就能通過X和Y訪問到唯一的A了。
總結:
鑽石繼承問題:
派生多箇中間子類的公共基類子物件,在繼承自多箇中間子類的匯聚子類物件中,存在多個例項。
在匯聚子類中,或通過匯聚子類物件,訪問公共基類的成員,會因繼承路徑的不同而導致不一致。
通過虛繼承,可以保證公共基類子物件在匯聚子類物件中,僅存一份例項,且為多箇中間子類子物件所共享。
虛繼承:
在繼承表中使用virtual關鍵字。
位於繼承鏈最末端的子類的建構函式負責構造虛基類子物件。
虛基類的所有子類(無論直接的還是間接的)都必須在其建構函式中顯式指明該虛基類子物件的構造方式,否則編譯器將選擇以預設方式構造該子物件。
虛基類的所有子類(無論直接的還是間接的)都必須在其拷貝建構函式中顯式指明以拷貝方式構造該虛基類子物件,否則編譯器將選擇以預設方式構造該子物件。