最近工作中某個軟件功能出現了退化,追查下來發現是一個類的成員變量沒有被正確的初始化。這個問題與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;
}

