这两天有个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. Largest Number

    Given a list of non negative integers, arrange them such that they form the largest number. For exam ...

  2. linux与windows的不同

    linux 严格区分大小写:linux 所有内容都以文件形式保存,包括用户和硬件:linux 不以文件后缀名来区分文件类型:但有一些便于管理员区分文件类型的约定俗称的后缀:windows下的程序不能直 ...

  3. 2009年到2013年甲子园ED

    2009年 2010年  最喜欢的一个!看过N遍 2011年 也不错! 2012年  超级好听啊~^_^比10年的还好,看过N+1遍……o(╯□╰)o 2013年春季甲子园 2013年夏季 印象最深的 ...

  4. 为 C# 代码生成 API 文档(译)

    原文地址:http://broadcast.oreilly.com/2010/09/build-html-documentation-for-y.html#comments Sandcastle 功能 ...

  5. linux keepalived+LVS 实现mysql 从库负载均衡

    前情提要: 参考链接: http://www.osyunwei.com/archives/7464.html ps:以上为本次操作的主要参考资料,非常感谢此文作者的贡献,我的随笔的主要目的是 说明在使 ...

  6. BizTalk 中使用 WCF-OracleDB adapter

    在使用BizTalk WCF-OracleDB adapter操作Oracle数据库时,遇到了一些问题,记录如下. 按照BizTalk的文档,目前BizTalk 2010支持的Oracle数据库版本如 ...

  7. 约瑟夫环(Josehpuse)的模拟

    约瑟夫环问题: 0,1,...,n-1这n个数字排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字,求出这个圆圈里剩下的最后一个数字. 这里给出以下几种解法, 1.用队列模拟 每次将前m-1个元 ...

  8. JAVA 几种引用类型学习

    1.对象的强.软.弱和虚引用    在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象.也就是说,只有对象处于可触及(reachable)状态,程序才能使用它.从J ...

  9. C++模板元编程 - 函数重载决议选择工具(不知道起什么好名)完成

    这个还是基于之前实现的那个MultiState,为了实现三种类型“大类”的函数重载决议:所有整数.所有浮点数.字符串,分别将这三种“大类”的数据分配到对应的Converter上. 为此实现了一些方便的 ...

  10. 112、两个Activity切换黑屏问题

    <activity android:name=".main.select.ActDoyenActivity" android:screenOrientation=" ...