这两天有个C++新手问了我一个问题,他的工程当中有一段代码执行不正确,不知道是什么原因。我调了一下,代码如果精简下来,大概是下面这个样子:

class IBaseA
{
public:
virtual void fnA() = ;
int m_nTestA;
}; class IBaseB
{
public:
virtual void fnB() = ;
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 ;
}

或许读者会觉得奇怪,中间为什么有个成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+(__RTC_CheckEsp) (811163h)

从这里我们可很清楚的看到结果是怎么回事了。

如果换成正确的转换方法,那执行过程是什么样子呢?事实上结果大家都知道,也知道其实是将IBaseB指针偏移到正确的位置。结合反汇编看;

    pBaseB = (IBaseB*)pTest;
008114E9 cmp dword ptr [pTest],
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],
mov ecx,dword ptr [ebp-100h]
0081150D mov dword ptr [pBaseB],ecx

好吧,现在过程很清晰了,说到底就是中间有个对eax加8的操作,直接将地址偏移到了正确的位置。

以上问题一言以蔽之,就是多重继承的时候,切不可先将this指针转换成其他类型,然后再转换成父类指针。犹如有个对象delete的时候,一定要确保指针是原来的类型再做delete,否则可能会导致析构函数没有调用而内存泄漏。

C++多重继承子类和父类指针转换过程中的一个易错点的更多相关文章

  1. 关于java学习中的一些易错点(基础篇)

    由JVM来负责Java程序在该系统中的运行,不同的操作系统需要安装不同的JVM,这样Java程序只需要跟JVM打交道,底层的操作由JVM去执行. JRE(Java Runtime Environmen ...

  2. java继承-子类调用父类的方法中包含子类重写的方法

    # 看题目是不是很绕,这个我也不知道怎么才能更简单的表达了... # 先看代码: public class Common { public static void main(String[] args ...

  3. iOS中 项目开发易错知识点总结

    点击return取消textView 的响应者 - (BOOL)textFieldShouldReturn:(UITextField *)textField { [_contactTextFiled  ...

  4. iOS中 项目开发易错知识点总结 韩俊强的博客

    每日更新关注:http://weibo.com/hanjunqiang  新浪微博! 点击return取消textView 的响应者 - (BOOL)textFieldShouldReturn:(UI ...

  5. js中正则表达式的易错点

    文章目录 1. 匹配符部分匹配规则 2. 分组匹配规则: 3. 注意^的不同用法 4. 不要忘记转义 5. 正则表达式对象中lastIndex属性 6. exec VS match 1. 匹配符部分匹 ...

  6. ROS::message_filters中的一个报错(mt::TimeStamp……)

    『方便检索』 ros::Time msg_time = mt::TimeStamp<typename mpl::at_c<Messages, i>::type>::value( ...

  7. C++ 类的多态三(多态的原理--虚函数指针--子类虚函数指针初始化)

    //多态的原理--虚函数指针--子类虚函数指针初始化 #include<iostream> using namespace std; /* 多态的实现原理(有自己猜想部分) 基础知识: 类 ...

  8. *C语言有关指针的变量声明中的几个易错点

    转至:http://my.oschina.net/ypimgt/blog/108265   Technorati 标签:  指针, typedef, const, define 我们都知道,至少听说过 ...

  9. C++虚函数表解析(图文并茂,非常清楚)( 任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法)good

    C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有“多种形态”,这是一种泛型技术 ...

随机推荐

  1. Linux线程-终止

    在前文讨论了线程创建的一些基本东西,这里讨论有哪些方法可以使线程终止,线程终止又是如何与创建所需的参数进行关联的. 一,正常终止 线程在执行完成之后,正常终止. 二,线程取消 2.1 线程取消的定义  ...

  2. iOS 的基本框架

    在iOS中框架是一个目录,包含了共享资源库,用于访问该资源库中储存的代码的头文件,以及图像.声音文件等其他资源.共享资源库定义应用程序可以调用的函数和方法.    iOS为应用程序开发提供了许多可使用 ...

  3. 第一次装TFS的曲折经历

    公司服务器之前TFS没装上,用的svn,实在忍受不了了,作为一个.net程序员怎么能用svn的,说动手就动手. 服务器的数据库是 SqlServer2008R2 本来想装TFS2013,后来试了一下, ...

  4. LINK : fatal error LNK1117: syntax error in option 'VERSION:1.6.5'

    今天在用vs2015编译ogre 1.6.5的代码时发生连接错误 LINK : fatal error LNK1117: syntax error in option 'VERSION:1.6.5'. ...

  5. Scrum2.0 项目基本完成

  6. Android Hotpatch系列之-给release包打Patch

    在默认debug包里面,是不对class做混淆的,所以Patch编写相对简单,但是应用在发布的时候都是release包,会对代码做混淆,此时class name ,field name,method ...

  7. UML常用图的几种关系的总结

    在UML的 类图中,常见的有以下几种关系: 泛化(Generalization),  实现(Realization), 关联(Association), 聚合(Aggregation), 组合(Com ...

  8. go语言的print

    代码: package main import ( "fmt" ) type point struct { x, y int } func main() { //Go 为常规 Go ...

  9. Python 3 条件、循环和assert、pass、del

    条件: if 条件:     语句块 elif:     语句块 else:     语句块 elif 表示 else if 这居然是合法的!!!1 < x < 2!!! >> ...

  10. 【mysql】压缩myisam数据表

    myisam引擎介绍 存储结构: MyISAM每张表被存放在三个文件: frm:表格定义: MYD(MYData):数据文件: MYI(MYIndex):索引文件:   存储空间: MyISAM可被压 ...