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----------->

_vfptr

 

m_nTestA

IBaseB----------->

_vfptr

 

m_nTestB

 

如果是轉換成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  
00811507  mov         ecx,dword ptr [ebp-100h]  
0081150D  mov         dword ptr [pBaseB],ecx 

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

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

 


免責聲明!

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



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