C++多重繼承子類和父類指針轉換過程中的一個易錯點

這兩天有個C++新手問了我一個問題,他的工程當中有一段代碼執行不正確,不知道是什麼原因。我調了一下,代碼如果精簡下來,大概是下面這個樣子:

class IBaseA
{
public:
    virtual void fnA() = 0;
    int m_nTestA;
};

class IBaseB
{
public:
    virtual void fnB() = 0;
    int m_nTestB;
};

class CTest : public IBaseA,public IBaseB
{
public:
    virtual void fnA(){ printf("fnA\n"); }
    virtual void fnB(){ printf("fnB\n"); }
};

int _tmain(int argc, _TCHAR* argv[])
{
    CTest *pTest = new CTest;
    void *p = (void*)pTest;
    IBaseA *pBaseA = (IBaseA*)p;
    pBaseA->fnA();

    IBaseB *pBaseB = (IBaseB*)p;
    pBaseB->fnB();

    pBaseB = (IBaseB*)pTest;
    pBaseB->fnB();
    getchar();
    return 0;
}

或許讀者會覺得奇怪,中間爲什麼有個成void*的轉換。因爲這段代碼是我把他代碼裏面最根本的問題精簡後的,結合到他的代碼上下文框架設計,中間確實是這樣,僅僅一眼看上去很容易忽略掉。事實上只需要簡單調試一下就會發現,指針變量pBaseB其實和pBaseA是完全一致的,而且調試發現其虛表地址也是一樣,但是如果這麼寫就不一樣了。
pBaseB = (IBaseB*)pTest;

那麼這個差異究竟是怎麼來的呢?這要從C++多重繼承的指針轉換說起。

事實上,C++內部指針轉換是很普遍的事情,比如無符號數到有符號數轉換,C++典型的就會報出一條警告,如果是設置了最高等級甚至直接報錯。子類指針轉換成父類指針,由於C++多重繼承用的場合並不是太多,所以大部分時候直接轉換就可以了,甚至按照以上轉換方法都沒問題。因爲C++指針轉換根本就是將原來對象的地址按照新的類型去解析了而已。

然而這種簡單的轉換對於C++的多重繼承卻有一個鮮爲人知的坑。對於以上代碼,CTest類所生成的對象內存佈局大概是這個樣子:

如果是轉換成IBaseA,那麼直接將pTest的內存地址首地址起,按照IBaseA解析就可以了,所以說pBaseA->fnA();執行沒問題。

但是對於IBaseB *pBaseB = (IBaseB*)p;,事實上還是將pTest的內存首地址直接按照IBaseA解析了。從內存佈局上看,第一個被誤以爲是IBaseB的地址。而執行pBaseB->fnB();這條語句,實際上是將這塊虛表中的第一個函數地址拿出來,然後直接調用了。由於兩個虛函數定義一致所以沒出問題,否則就直接崩潰了。

從反彙編我們也可以看到,整個執行過程就是直接將p賦值給pBaseB,然後取pBaseB的前4個字節,也就是虛表地址,然後再取虛表地址的前4個字節,也就是第一個虛函數的地址。然後從008114DB地址開始,傳入this指針,保存虛函數地址到eax再調用。

IBaseB *pBaseB = (IBaseB*)p;
008114CE  mov         eax,dword ptr [p]  
008114D1  mov         dword ptr [pBaseB],eax  
    pBaseB->fnB();
008114D4  mov         eax,dword ptr [pBaseB]  
008114D7  mov         edx,dword ptr [eax]  
008114D9  mov         esi,esp  
008114DB  mov         ecx,dword ptr [pBaseB]  
008114DE  mov         eax,dword ptr [edx]  
008114E0  call        eax  
008114E2  cmp         esi,esp  
008114E4  call        @ILT+350(__RTC_CheckEsp) (811163h)

從這裏我們可很清楚的看到結果是怎麼回事了。

如果換成正確的轉換方法,那執行過程是什麼樣子呢?事實上結果大家都知道,也知道其實是將IBaseB指針偏移到正確的位置。結合反彙編看;

pBaseB = (IBaseB*)pTest;
008114E9  cmp         dword ptr [pTest],0  
008114ED  je          wmain+0ADh (8114FDh)  
008114EF  mov         eax,dword ptr [pTest]  
008114F2  add         eax,8  
008114F5  mov         dword ptr [ebp-100h],eax  
008114FB  jmp         wmain+0B7h (811507h)  
008114FD  mov         dword ptr [ebp-100h],0  
 mov         ecx,dword ptr [ebp-100h]  
0081150D  mov         dword ptr [pBaseB],ecx

好吧,現在過程很清晰了,說到底就是中間有個對eax加8的操作,直接將地址偏移到了正確的位置。

以上問題一言以蔽之,就是多重繼承的時候,切不可先將this指針轉換成其他類型,然後再轉換成父類指針。猶如有個對象delete的時候,一定要確保指針是原來的類型再做delete,否則可能會導致析構函數沒有調用而內存泄漏。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章