本文目的
前幾天在寫程序時,發現一個多重繼承類,調用virtual函數會出現一個問題,該問題比較隱晦(因為不會引起程序core dump等嚴重的效果,我是很偶然的在單元測試中發現的),不容易定位,但是如果出現,可能對程序邏輯會帶來致命的問題。
一個例子
#include <iostream> using namespace std; class Base1{ public: virtual void foo1() {}; }; class Base2{ public: virtual void foo2() {}; }; class MI : public Base1, public Base2{ public: virtual void foo1 () {cout << "MI::foo1" << endl;} virtual void foo2 () {cout << "MI::foo2" << endl;} }; int main(){ MI oMI; Base1* pB1 = &oMI; pB1->foo1(); Base2* pB2 = (Base2*)(pB1); // 指針強行轉換,沒有偏移 pB2->foo2(); pB2 = dynamic_cast<Base2*>(pB1); // 指針動態轉換,dynamic_cast幫你偏移 pB2->foo2(); return 0; }
你會認為屏幕上會輸出什么?是下面的結果嗎?
MI::foo1 MI::foo2 MI::foo2 |
這樣認為沒有什么不對的,因為C++的多態性保證用父類指針可以正確的找到子類實現,並調用。所以會有上面的輸出。
但是,現實卻不是這樣,下面是真實的輸出:
(以上實現在VC 2005和Linux Gcc 4.1.2效果一致)
為什么
為什么會出現上面的情況呢,上面代碼中的注釋部分也許解釋了,這里再來詳細的來分析一下。
首先,C++使用一種稱之為vtable(google “vtable” for more details)的東西實現virtual函數多態調用。vtable每個類中都有一個,該類的所有對象公用,由編譯器幫你生成,只要有virtual函數的類,均會有vtable。在繼承過程中,由於類Base1和類Base2都有vtable,所以類MI繼承了兩個vtable。簡單的分析一下對象oMI內存結構,如下:
0 vtable_address_for_Base1 –> [MI::foo1, NULL] 4 vtable_address_for_Base2 –> [MI::foo2, NULL] |
其實很簡單,就兩個vtable的指針,0和4代表相對地址,指針地址大小為4。
pB1的值為0(pB1 == 0),所以調用“pB1->foo1()”時,可以正確的找到MI::fool這個函數執行。
但是當使用強行轉換,將pB1轉給pB2,那么實質上pB2的值也是0(pB2 == 0),當調用“pB2->foo2()”時,無法在第一個vtalbe中找到對應的函數,但是卻不報錯,而是選擇執行函數MI::foo1,不知道為什么會有這種行為,但是這種行為卻十分惡心,導致結果無法預期的(最后調用的函數會與函數申明的循序有關),不太會引起注意,使得bug十分隱晦。
可以設想,當一個有復雜的業務邏輯的程序,而類似這種函數調用和指針強行轉換分布在不同的函數或模塊中,可想而知,bug定位十分困難。
當使用動態轉換時,也就是“pB2 = dynamic_cast<Base2*>(pB1)”,dynamic_cast函數會根據尖括號中的類型進行指針偏移,所以pB2的值為4(pB2 == 4),這樣調用“pB2->foo2()”就會按照期望的方式執行。
結論
上面的現象在單繼承中是不會出現的,因為只有一個vtable(子類的virtual函數會自動追加到第一個父類的vtable的結尾)。所以不會出現上面的現象,而多重繼承卻出現了上面的想象,所以需要注意以下兩點:
1. 多重繼承需要慎用
2. 類型轉換盡量采用c++內置的類型轉換函數,而不要強行轉換