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