首先,何為鑽石繼承,顧名思義,在類的繼承過程中,繼承結構是一個類似菱形(鑽石)的結構就屬於鑽石繼承,如下:
這是一個最簡單的鑽石繼承。實際上,在復雜的繼承表中,只要子類按不同的繼承路徑回溯到基類有菱形結構,均屬鑽石繼承。下面先看一個例子,鑽石繼承在C++程序設計中帶來的問題。
1 //diamond.cpp 2 #include<iostream> 3 using namespace std; 4 class A{ 5 public: 6 A (int x) : m_x(x) {} 7 int m_x; 8 }; 9 class B : public A { 10 public: 11 B (int x) : A(x) {} 12 void set(int x) { 13 this -> m_x = x; 14 } 15 }; 16 class C : public A { 17 public: 18 C (int x) : A(x) {} 19 int get(void) { 20 return this -> m_x; 21 } 22 }; 23 class D : public B,public C { 24 public: 25 D (int x) : B(x),C(x) {} 26 }; 27 int main(void) { 28 D d(10); 29 d.set(20); 30 cout << d.get() << endl; 31 return 0; 32 } 33
這樣的運行結果是10?還是20呢?結果是10,為什么?!明明sets的是20,為什么get的還是10呢?
要解釋這個問題那酒必須要先搞清楚,d對象在內存中是如何存放的,是怎樣布局的。每一個子類都會有一個內存視圖,在子類里都包含了它的基類子對象,下面是創建是d對象時,d對象在內存中的存放形式。
包含一個B類的基類子對象和一個C類型基類子對象,而B和C里各自有一個A類型基類子對象,所以可以看到,在d的內存布局中有兩個A類型基類子對象。
set函數是類B的成員函數,在執行set函數時,this指針指向B(其實也是指向A,B從A繼承,A存在B中的首地址),所以set執行后,改變的是B里的A類基類子對象的數據成員的值。同理,get函數得到的是C里A類基類子對象的數據成員的值。這樣就可以理解這樣的運行結果了。所謂鑽石繼承問題,就是公共基類對象在我們最終的子類對象中有多個副本,多份拷貝,當我們沿着不同的繼承路徑去訪問公共基類子對象時結果會出現不一致。
而我們應該怎樣解決這樣的問題呢?采用虛繼承。我們所期望的d的存儲形式:
我們需要按如下方式修改代碼:
class B : virtual public A //虛繼承 class C : virtual public A //虛繼承 D(int x) : B(x),C(x),A(x) {}
這樣就解決了。
在這個過程中,A對象只在D的初始化表中A(x)進行構造(虛基類最先被構造),而在B和C的初始化表中不再對A進行構造(實際上是都有一個指針指向了D中的A(x),來對A進行構造)。
鑽石繼承,在訪問公共基類成員函數時,如果不是虛繼承,還會引起二義性的錯誤。代碼如下:
1 //diamond.cpp 2 #include<iostream> 3 using namespace std; 4 class A{ 5 public: 6 A (int x) : m_x(x) {} 7 void foo() { 8 cout << "A::foo()" << endl; 9 } 10 int m_x; 11 }; 12 class B : public A { 13 public: 14 B (int x) : A (x) {} 15 void set (int x) { 16 this -> m_x = x; 17 } 18 }; 19 class C : public A { 20 public: 21 C (int x) : A (x) {} 22 int get (void) { 23 return this -> m_x; 24 } 25 }; 26 class D : public B,public C { 27 public: 28 D (int x) : B(x), C(x), A(x) {} 29 }; 30 int main(void) { 31 D d(10); 32 d.set (20); 33 cout << d.get() << endl; 34 d.foo(); 35 return 0; 36 }
編譯器會報錯:對成員'foo()'的請求有歧義,備選為 void A::foo() void A::foo()
依舊用對象d的內存視圖來理解,在構建d對象時,里面存在兩個A類基類子對象,盡管成員函數不存放在類中而在代碼段,並且只會有一份,但是編譯器不知道,他會作為兩個繼承函數來處理,用d.foo()來訪問時,編譯器便不知道訪問的是哪一個基類子對象里的foo(),所以備選項都是void A::foo()。
而通過虛繼承可以避免通過最終子類訪問其繼承自公共基類的成員函數時引發的名字沖突問題。