C++多重继承子类和父类指针转换过程中的一个易错点
这两天有个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++多重继承子类和父类指针转换过程中的一个易错点的更多相关文章
- 关于java学习中的一些易错点(基础篇)
由JVM来负责Java程序在该系统中的运行,不同的操作系统需要安装不同的JVM,这样Java程序只需要跟JVM打交道,底层的操作由JVM去执行. JRE(Java Runtime Environmen ...
- java继承-子类调用父类的方法中包含子类重写的方法
# 看题目是不是很绕,这个我也不知道怎么才能更简单的表达了... # 先看代码: public class Common { public static void main(String[] args ...
- iOS中 项目开发易错知识点总结
点击return取消textView 的响应者 - (BOOL)textFieldShouldReturn:(UITextField *)textField { [_contactTextFiled ...
- iOS中 项目开发易错知识点总结 韩俊强的博客
每日更新关注:http://weibo.com/hanjunqiang 新浪微博! 点击return取消textView 的响应者 - (BOOL)textFieldShouldReturn:(UI ...
- js中正则表达式的易错点
文章目录 1. 匹配符部分匹配规则 2. 分组匹配规则: 3. 注意^的不同用法 4. 不要忘记转义 5. 正则表达式对象中lastIndex属性 6. exec VS match 1. 匹配符部分匹 ...
- ROS::message_filters中的一个报错(mt::TimeStamp……)
『方便检索』 ros::Time msg_time = mt::TimeStamp<typename mpl::at_c<Messages, i>::type>::value( ...
- C++ 类的多态三(多态的原理--虚函数指针--子类虚函数指针初始化)
//多态的原理--虚函数指针--子类虚函数指针初始化 #include<iostream> using namespace std; /* 多态的实现原理(有自己猜想部分) 基础知识: 类 ...
- *C语言有关指针的变量声明中的几个易错点
转至:http://my.oschina.net/ypimgt/blog/108265 Technorati 标签: 指针, typedef, const, define 我们都知道,至少听说过 ...
- C++虚函数表解析(图文并茂,非常清楚)( 任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法)good
C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有“多种形态”,这是一种泛型技术 ...
随机推荐
- M2: XAML Controls(2)
在前小节中,我们在Card程序的主界面中加入了简单的XAML控件, 本小节将在其基础上进行优化,使界面看上去更加美观.本小节用到了Grid Control, Border Control,以及XAML ...
- Day20_IO第二天
1.IO体系总图 2.IO体系--字节流 记忆路线:输入输出流前面加File和Buffered,这样就全记住了 3.表达式解释 表达式:由变量和常量通过运算符连接起来的式子,单个的常量和变量也是表达式 ...
- 更新EF,EF 报错
在项目中,对一个视图进行了更新,增加了一个字段,然后需要更新EF访问,可是往往会报错, 查看映射关系发现EF将字段映射为主键,而视图没有进行ISNULL处理. 可以有两种处理方式: 1:修改视图对字段 ...
- PostgreSQL数据库基本配置
一.安装 首先安装PostgreSQL客户端: sudo apt-get install postgresql-client 然后,安装PostgreSQL服务器: sudo apt-get inst ...
- C++ Pirmer : 第十五章 : 面向对象程序设计之基类和派生的定义、类型转换与继承与虚函数
基类和派生类的定义以及虚函数 基类Quote的定义: classs Quote { public: Quote() = default; Quote(cosnt std::string& bo ...
- iOS红马甲项目开发过程Bug总结(1)
在上线审核时,重新检测自己的app发现报错:"was compiled with optimization - steppingmay behave oddly; variables may ...
- UltraEdit 所有快捷键 说明
快捷键 命令 说明 -------------------+------- ...
- Yii2.0中文开发向导——Yii2中多表关联查询(join、joinwith)
我们用实例来说明这一部分 表结构 现在有客户表.订单表.图书表.作者表, 客户表Customer (id customer_name) 订单表Order (id order_ ...
- Jquery tab 选项卡 无刷新切换
转载的 演示地址:http://www.freejs.net/demo/29/index.html 首页>>TAB标签>>jquery实现简单的Tab切换菜单(2013-09- ...
- 电源开关IC
RT9701:IO控制的电源开关.宽输入电压(2.2~6v),1.1A的连续输出电流.用在USB开关电压,热插拔和电池电池充电器的场合应用