先看一段在建構函式中直接調用虛函數的代碼:
1 #include <iostream> 2 3 class Base 4 { 5 public: 6 Base() { Foo(); } ///< 列印 1 7 8 virtual void Foo() 9 {10 std::cout << 1 << std::endl;11 }12 };13 14 class Derive : public Base15 {16 public:17 Derive() : Base(), m_pData(new int(2)) {}18 ~Derive() { delete m_pData; }19 20 virtual void Foo()21 {22 std::cout << *m_pData << std::endl;23 }24 private:25 int* m_pData;26 };27 28 int main()29 {30 Base* p = new Derive();31 delete p;32 return 0;33 }
這裡的結果將列印:1。
這表明第6行執行的的是Base::Foo()而不是Derive::Foo(),也就是說:虛函數在建構函式中“不起作用”。為什麼。
當執行個體化一個衍生類別對象時,首先進行基類部分的構造,然後再進行衍生類別部分的構造。即建立Derive對象時,會先調用Base的建構函式,再調用Derive的建構函式。
當在構造基類部分時,衍生類別還沒被完全建立,從某種意義上講此時它只是個基類對象。即當Base::Base()執行時Derive對象還沒被完全建立,此時它被當成一個Base對象,而不是Derive對象,因此Foo綁定的是Base的Foo。
C++之所以這樣設計是為了減少錯誤和Bug的出現。假設在建構函式中虛函數仍然“生效”,即Base::Base()中的Foo();所調用的是Derive::Foo()。當Base::Base()被調用時衍生類別中的資料m_pData還未被正確初始化,這時執行Derive::Foo()將導致程式對一個未初始化的地址解引用,得到的結果是不可預料的,甚至是程式崩潰(訪問非法記憶體)。
總結來說:基類部分在衍生類別部分之前被構造,當基類建構函式執行時衍生類別中的資料成員還沒被初始化。如果基類建構函式中的虛函數調用被解析成調用衍生類別的虛函數,而衍生類別的虛函數中又訪問到未初始化的衍生類別資料,將導致程式出現一些未定義行為和bug。
對於這一點,一般編譯器會給予一定的支援。如果將基類中的Foo聲明成純虛函數時(看下面代碼),編譯器可能會:在編譯時間給出警告、連結時給出符號未解析錯誤(unresolved external symbol)。如果能產生可執行檔,運行時一定出錯。因為Base::Base()中的Foo總是調用Base::Foo,而此時Base::Foo只聲明沒定義。大部分編譯器在連結時就能識別出來。
1 #include <iostream> 2 3 class Base 4 { 5 public: 6 Base() { Foo(); } ///< 可能的結果:編譯警告、連結出錯、執行階段錯誤 7 8 virtual void Foo() = 0; 9 };10 11 class Derive : public Base12 {13 public:14 Derive() : Base(), m_pData(new int(2)) {}15 ~Derive() { delete m_pData; }16 17 virtual void Foo()18 {19 std::cout << *m_pData << std::endl;20 }21 private:22 int* m_pData;23 };24 25 int main()26 {27 Base* p = new Derive();28 delete p;29 return 0;30 }
如果編譯器都能夠在編譯或連結時識別出這種錯誤調用,那麼我們犯錯的機會將大大減少。只是有一些比較不直觀的情況(看下面代碼),編譯器是無法判斷出來的。這種情況下它可以產生可執行檔,但是當程式運行時會出錯。
1 #include <iostream> 2 3 class Base 4 { 5 public: 6 Base() { Subtle(); } ///< 執行階段錯誤(pure virtual function call) 7 8 virtual void Foo() = 0; 9 void Subtle() { Foo(); }10 };11 12 class Derive : public Base13 {14 public:15 Derive() : Base(), m_pData(new int(2)) {}16 ~Derive() { delete m_pData; }17 18 virtual void Foo()19 {20 std::cout << *m_pData << std::endl;21 }22 private:23 int* m_pData;24 };25 26 int main()27 {