C++ 鑽石繼承與虛繼承
首先,何為鑽石繼承,顧名思義,在類的繼承過程中,繼承結構是一個類似菱形(鑽石)的結構就屬於鑽石繼承,如下:
這是一個最簡單的鑽石繼承。實際上,在複雜的繼承表中,只要子類按不同的繼承路徑回溯到基類有菱形結構,均屬鑽石繼承。下面先看一個例子,鑽石繼承在C++程式設計中帶來的問題。
//diamond.cpp #include<iostream> using namespace std; class A{ public: A (int x) : m_x(x) {} int m_x; }; class B : public A { public: B (int x) : A(x) {} void set(int x) { this -> m_x = x; } }; class C : public A { public: C (int x) : A(x) {} int get(void) { return this -> m_x; } }; class D : public B,public C { public: D (int x) : B(x),C(x) {} }; int main(void) { D d(10); d.set(20); cout << d.get() << endl; return 0; }
這樣的執行結果是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進行構造)。
鑽石繼承,在訪問公共基類成員函式時,如果不是虛繼承,還會引起二義性的錯誤。程式碼如下:
//diamond.cpp #include<iostream> using namespace std; class A{ public: A (int x) : m_x(x) {} void foo() { cout << "A::foo()" << endl; } int m_x; }; class B : public A { public: B (int x) : A (x) {} void set (int x) { this -> m_x = x; } }; class C : public A { public: C (int x) : A (x) {} int get (void) { return this -> m_x; } }; class D : public B,public C { public: D (int x) : B(x), C(x), A(x) {} }; int main(void) { D d(10); d.set (20); cout << d.get() << endl; d.foo(); return 0; }
編譯器會報錯:對成員'foo()'的請求有歧義,備選為 void A::foo() void A::foo()
依舊用物件d的記憶體檢視來理解,在構建d物件時,裡面存在兩個A類基類子物件,儘管成員函式不存放在類中而在程式碼段,並且只會有一份,但是編譯器不知道,他會作為兩個繼承函式來處理,用d.foo()來訪問時,編譯器便不知道訪問的是哪一個基類子物件裡的foo(),所以備選項都是void A::foo()。
而通過虛繼承可以避免通過最終子類訪問其繼承自公共基類的成員函式時引發的名字衝突問題。