• 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
    "); }
        virtual void fnB(){ printf("fnB
    "); }
    };
    
    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,否则可能会导致析构函数没有调用而内存泄漏。

  • 相关阅读:
    提交代码报错不同方式
    pow log 与 (int)
    优先队列的创建
    积性函数
    静态主席树,动态主席树(一种可持久化线段树)
    codeblocks 输入、输出文件的位置
    后缀自动机
    BellmanFord 最短路
    struct
    hdu1501 动态规划
  • 原文地址:https://www.cnblogs.com/mod109/p/5989088.html
Copyright © 2020-2023  润新知