在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關鍵字。
位于繼承鍊最末端的子類的構造函數負責構造虛基類子對象。
虛基類的所有子類(無論直接的還是間接的)都必須在其構造函數中顯式指明該虛基類子對象的構造方式,否則編譯器将選擇以預設方式構造該子對象。
虛基類的所有子類(無論直接的還是間接的)都必須在其拷貝構造函數中顯式指明以拷貝方式構造該虛基類子對象,否則編譯器将選擇以預設方式構造該子對象。