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

时间: 2024-10-10 21:23:51

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

C++的子类与父类强制转换产生的问题

近日,在项目的一个类中如果碰上想要将子类强制转换成父类,然后再调用其父类版本的virtual虚函数. 就会出现gcc编译错误提示:error: ld returned 1 exit status gcc提示原因:在link过程中,发现了无法找到的reference symbol, 该reference symbol是此父类的拷贝构造函数. 由此,我推断问题的原因是:在子类转换成父类的过程中,默认地调用了父类的拷贝构造函数.而由于项目中针对大部分类都使用了DISALLOW_COPY_AND_ASS

腾讯2011一道父类指针和子类指针转化的题目

class ClassA { public: virtual ~ClassA(){} virtual void FunctionA(){} }; class ClassB { public: virtual void FunctionB(){} }; class ClassC :public ClassA , public ClassB { }; 关于pA,pB,pC的取值,下面的描述中正确的是: A.pA,pB,pC的取值相同. B.pC=pA+pB C.pA,pB不相同 D.pC不等于pA也

子类指针转换成父类指针时,指针值会变化

指针转换在C++中再正常不过了,特别是子类向父类的转换,一般情况下转换后指针值是不会变的.但是某些特殊情况,子类指针转换成父类指针,指针值会产生一定的偏移量,至于为什么就不清楚了.要确保指针值不变,可以先将指针转成void*,再根据实际情况强制转换成需要的类型就可以了. 本文地址:http://www.cnblogs.com/vcpp123/p/5902866.html 参考文章: C++多重继承下的指针类型转换 http://www.veryhuo.com/a/view/52953.html

父类指针指向子类实例:同名非虚函数

场景描述:如果父类指针指向子类的实例,并且调用的函数是一个子类和父类同名的函数,并且这个函数没有被定义为虚函数,肯定无法通过父类指针调用子类的接口.测试代码如下: class CFather{ public:      void Display()     {         cout<<"Father display"<<endl;     } }; class CSon:public CFather { public:      void Display()

多重继承下 Virtual Function 的语意

在多重继承中支持 virtual function, 其复杂度围绕在第二个及后继的 base classes 上, 以及必须在执行期调整 this 指针这一点, 以以下的 class 体系为例: class Base1 { public: Base1(); virtual ~Base1(); virtual void SpeakClearly(); virtual Base1* Clone() const; protected: float data_base1; }; class Base2

面向对象-继承,顺序查找,派生,子类访问父类,菱形继承

OOP的三大特征(优势) 1.封装 2.继承 3.多态 继承:是两个对象之间产生的一种关系 a继承b 在OOP的程序中继承是描述类与类之间的一种关系继承的好处:程序中 一个类a继承另一个类b a就可以使用B的属性和方法具体的说:继承极大的提高了代码的重用性继承描述的是 什么 是什么的关系 注意:在使用集成时,要自己分析.类与类之间的关系 不应该违反显示生活中的原则在使用继承的时候一定是先抽象 在继承抽象:抽取一堆类共同拥有的内容 形成一个新的抽象的概念(也称为公共基类) 这个过程就叫做 抽象注意

8.python之面相对象part.5(子类调用父类的方法,以及super关键字)

python中子类要调用父类的某个方法,在python早期版本中是这样实现的: class A: def __init__(self): print "enter A" print "leave A" class B(A): def __init__(self): print "enter B" A.__init__(self) print "leave B" b = B() >>>enter B enter

【C/C++学院】0823-静态联合编译与动态联合编译/父类指针子类指针释放/虚函数/纯虚函数概念以及虚析构函数/抽象类与纯虚函数以及应用/虚函数原理/虚函数分层以及异质链表/类模板的概念以及应用

静态联合编译与动态联合编译 #include <iostream> #include <stdlib.h> //散列 void go(int num) { } void go(char *str) { } //class //::在一个类中 class A { public: void go(int num) { } void go(char *str) { } }; void main() { ///auto p = go;编译的阶段,静态联编 void(*p1)(char *s

基类与派生类,父类指针指向子类对象

先看一段代码: 1 #include<iostream> 2 3 using namespace std; 4 5 class Base{ 6 public: 7 Base() { cout<<"Base Creted"<<endl; } 8 ~Base() { cout<<"Base Destroyed"<<endl; } 9 }; 10 11 class Derived: public Base { 1