C++多重继承下一个子类和父类指针转换的bug

Posted 繁星的技术博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++多重继承下一个子类和父类指针转换的bug相关的知识,希望对你有一定的参考价值。

这两天有个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,否则可能会导致析构函数没有调用而内存泄漏。

 

以上是关于C++多重继承下一个子类和父类指针转换的bug的主要内容,如果未能解决你的问题,请参考以下文章

第53课 被遗弃的多重继承

第二小节之面向对象下

C++ 当子类的方法和父类的方法重名时...

被遗弃的多重继承(四十五)

第五十三课被遗弃的多重继承(上)

代码重用和接口重用