C++多重繼承要慎用!


本文目的

前幾天在寫程序時,發現一個多重繼承類,調用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++的多態性保證用父類指針可以正確的找到子類實現,並調用。所以會有上面的輸出。

但是,現實卻不是這樣,下面是真實的輸出:

clip_image002

(以上實現在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++內置的類型轉換函數,而不要強行轉換

 

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM