最近工作中某個軟件功能出現了退化,追查下來發現是一個類的成員變量沒有被正確的初始化。這個問題與C++存在虛繼承的情況下派生類構造函數的寫法有關。在此說明一下錯誤發生的原因,希望對更多的人有幫助。
我們代碼中存在虛繼承的類的繼承結構與下圖類似,並不是教科書中經典的菱形結構。從 Intermediate1 和 Intermediate3 到Base2 的繼承是虛繼承。Base1 和 Base2 包含一些成員變量,並提供了相應的構造函數接受指定的初始化值。Base2 還有一個缺省構造函數,把其成員變量都初始化為0。Intermediate1,2,3 也都提供了一個構造函數接受指定的初始化值,並在在初始化列表里調用Base1和Base2的構造函數完成初始化。
一位同事在做重構時,不小心把Final的代碼改成了:
class Final : public Intermediate2, public Intermediate3 { public: Final (int a, int b, int c) : Intermediate2(a, b, c), Intermediate3(b, c) { } };
class Intermediate1 : public Base1, virtual public Base2 { public: Intermediate1(int a, int b, int c) : Base1(a), Base2(b, c) { } }; class Intermediate2 : public Intermediate1 { public: Intermediate2(int a, int b, int c) : Intermediate1(a, b, c), Base2(b, c) { } }; class Intermediate3 : virtual public Base2 { public: Intermediate3(int b, int c) : Base2(b, c) { } };
看上去,Final的構造函數將調用Intermediate2 和 Intermediate3的構造函數分別將m_a, m_b 和 m_c初始化成指定的值。可是,運行時發現m_b和m_c的值是0!明顯,這是調用了Base2的缺省構造函數。
原來,C++的規則是:如果在繼承鏈上存在虛繼承的基類,則最底層的子類要負責完成該虛基類部分成員的構造。我們可以顯式調用虛基類的構造函數完成初始化。如果不顯式調用虛基類的構造函數,則編譯器會調用虛基類的缺省構造函數。如果不顯式調用虛基類的構造函數,而虛基類沒有定義缺省構造函數,則會出現編譯錯誤。這條規則的原因是:如果不這樣做,則虛基類部分會在存在的多個繼承鏈條上被多次初始化。
很多時候,對於繼承鏈上的中間類,我們也會在其構造函數中顯式調用虛基類的構造函數,因為一旦有人要創建這些中間類的對象,我們也要保證它們得到正確的初始化。
所以,如果我們要把m_b和m_c初始化成指定的值,Final的構造函數的正確寫法應該是這樣:
Final (int a, int b, int c) : Base2(b, c), Intermediate2(a, b, c), Intermediate3(b, c) { }
完整的測試程序如下所示,有興趣的同學可以自行編譯運行一下。也可以在調試器中單步運行Final的構造函數,看看前后兩種寫法分別是調用了Base2的哪個構造函數。
#include "stdafx.h" #include <iostream> using namespace std; class Base1 { public: Base1(int a): m_a(a) {} protected: int m_a; }; class Base2 { public: Base2(int b, int c): m_b(b), m_c(c) {} Base2() : m_b(0), m_c(0) {} protected: int m_b; int m_c; }; class Intermediate1 : public Base1, virtual public Base2 { public: Intermediate1(int a, int b, int c) : Base1(a), Base2(b, c) { } }; class Intermediate2 : public Intermediate1 { public: Intermediate2(int a, int b, int c) : Intermediate1(a, b, c), Base2(b, c) { } }; class Intermediate3 : virtual public Base2 { public: Intermediate3(int b, int c) : Base2(b, c) { } }; class Final : public Intermediate2, public Intermediate3 { public: Final (int a, int b, int c) : Base2(b, c), Intermediate2(a, b, c), Intermediate3(b, c) { } void Print() { cout<<m_a<<", "<<m_b<<", "<<m_c<<endl; } }; int _tmain(int argc, _TCHAR* argv[]) { Final finalObj(1, 2, 3); finalObj.Print(); return 0; }