在C++中,指针的类型转换是经常发生的事情,比如将派生类指针转换为基类指针,将基类指针转换为派生类指针。指针的本质其实就是一个整数,用以记录进程虚拟内存空间中的地址编号,而指针的类型决定了编译器对其指向的内存空间的解释方式。
基于上面的理解,我们似乎可以得出一个结论,C++中对指针进行类型转换,不会改变指针的值,只会改变指针的类型(即改变编译器对该指针指向内存的解释方式),但是这个结论在C++多重继承下是 不成立的。
看下面一段代码:

using namespace std;
class CBaseA
{
public:
char m_A[32];
};
class CBaseB
{
public:
char m_B[64];
};
class CDerive : public CBaseA, public CBaseB
{
public:
char m_D[128];
};
int main()
{
auto pD = new CDerive;
auto pA = (CBaseA *)pD;
auto pB = (CBaseB *)pD;
cout << pA << '\n' << pB << '\n' << pD << endl;
cout << (pD == pB) << endl;
}
这段代码的输出是:
0x9f1080
0x9f10a0
0x9f1080
1
可以看出,指向同一个堆上new出来的内存指针,在经过类型转换之后,其值会发生改变。究其原因,要从C++中多重继承的内存布局说起。
new CDerive;执行之后,生成的内存布局如下:
同时我们注意到,pB与pD的指针差值正好是CBaseA占用的内存大小32字节,而pA与pD都指向了同一段地址。这是因为,将一个派生类的指针转换成某一个基类指针,编译器会将指针的值偏移到该基类在对象内存中的起始位置。
可是为什么C++要这样设计呢?
试想,沿用上面的场景,如果pB和pA都指向对象的首地址,那么使用pB指针来定位CBaseB的成员变量m_B时,编译器应该将pB指针偏移32个字节,从而跳过CBaseA的内存部分。而pB指针如果是这样产生的auto pB = new CBaseB;,那么使用pB指针来定位CBaseB的成员变量m_B时,偏移量应该为0。
关键在于对于一个指针而言,编译器不关心也无法知道该指针的来源(一种极端情况,指针是从其他模块传递过来的),而只是把它视为一个有指针类型的整数。所以对于CBaseB类型的指针,取CBaseB的成员变量m_B时,偏移量应该通通为0,这是通过CBaseB的类声明就可以统一决策的事情。
说到这里,pD和pB的指针地址为什么不一样大家应该清楚了,可是为什么下面的代码输出是1呢?
cout << (pD == pB) << endl;
输出1表示pD和pB是相等的,而刚刚我们才说明了,pD和pB的地址是相差了32个字节的。
其实这也是编译器为大家屏蔽了这种指针的差异,当编译器发现一个指向派生类的指针和指向其某个基类的指针进行==运算时,会自动将指针做隐式类型提升已屏蔽多重继承带来的指针差异。因为两个指针做比较,目的通常是判断两个指针是否指向了同一个内存对象实例,在上面的场景中,pD和pB虽然指针值不等,但是他们确确实实都指向了同一个内存对象(即new CDerive;产生的内存对象 ),所以编译器又在此处插了一脚,让我们可以安享==运算的上层语义。
from:http://www.veryhuo.com/a/view/52953.html
多重继承是C++的特性之一,但在比较新的Java和C#中被摒弃,因为多重继承在类型转换中会出现一些有意思的现象。
下面是虚方法多重继承
- #include <iostream>
- #define interface struct
- using namespace std;
- interface IA {
- virtual void FA() = 0;
- };
- interface IB {
- virtual void FB() = 0;
- };
- class CA:
- public IA,public IB
- {
- virtual void FA()
- {
- cout<<"FA"<<endl;
- }
- virtual void FB()
- {
- cout<<"FB"<<endl;
- }
- };
- int main()
- {
- CA* pCA = new CA();
- IA* pIA = static_cast<IA*>(pCA);
- IB* pIB = static_cast<IB*>(pCA);
- return 0;
- }
在程序中加入断点,调试运行,发现pIA的值为0x003a53f8,
而pIB的值为0x003a53fc,也就是说pIB比pIA向后偏移了4个字节
派生类对象指针转换为不同基类对象指针时,编译器会做一些小小的手脚
按照派生类声明的继承顺序,转换为第一基类时指针不变,以后依次向后偏移前一基类所占字节数
这里第一基类IA没有数据成员,偏移量是虚表指针的大小-4字节,
如果有数据成员,还要加上数据成员的大小
注:基于数据对齐原则,在32位系统不足4字节算做4字节
为什么会这样呢?
对于虚方法对象来说,对象开始位置是虚表指针,如果多继承的话
每一个基类都有各自的虚表指针和数据成员,这样派生类对象如何构造呢?
派生类的内存结构如下:
基类A虚表指针 基类A数据成员 基类B虚表指针 基类B数据成员 。。。。。。 派生类数据成员
这样,派生类对象指针在向上转型时,要正确的获取基类的虚方法和数据成员,
只有将指针改变。
代码如下:
- class A{
- public:
- A(){_a = 0x00;}
- virtual ~A(){}
- virtual void printA(){}
- virtual A* clone()const{return NULL;}
- int _a;
- };
- class B{
- public:
- B(){_b = 0x11;}
- virtual ~B(){}
- virtual void printB(){}
- virtual B* clone()const{return NULL;}
- int _b;
- };
- class C:public A, public B{
- public:
- C(){_c = 0x22;}
- virtual ~C(){}
- virtual C* clone()const{return new C;}
- int _c;
- };
- int _tmain()
- {
- A a;
- B b;
- C c;
- B* pb = &c;
- <span style="color:#ff0000;">B* pc = pb->clone();</span>
- }
首先看一下C对象的布局:
__vfptr |
_a |
__vfptr |
_b |
_c |
红色为C对象中的A子对象,绿色为C对象中的B子对象。
当将c的地址赋给pb时,编译器会将c的地址减8,所以pb实际指向的是绿色部分。
接下来就是进行函数调用了,标红语句会被翻译成如下形式:
(*pb->__vfptr[1])(this);假设clone在虚函数表中的索引为1
由于多态性,该条语句实际要调用的代码是C::clone。所以this的地址是要调整的,在该例中,this需要减8以指向实际的C对象。又因为B::clone的函数原型,返回值也需要调整,VC通过对虚函数插入一个包装函数来做这些工作。下面是this指针调整的函数:
[thunk]:C::clone`adjustor{8}':这个函数是为了进行this调整为了在给类C的虚函数加的包装
0042EF50 sub ecx,8 编译器知道在C对象中由第2个基类指针调用时,this指针要上移8个字节
0042EF53 jmp C::clone (42C31Bh)
然而,返回值也是需要调整的,因为按B中clone的原型,C::clone的返回值需要+8,所以上面jmp跳转的仍然是一个包装函数:
C::clone:
0042EF60 push ebp
0042EF61 mov ebp,esp
0042EF63 sub esp,0D4h
0042EF69 push ebx
0042EF6A push esi
0042EF6B push edi
0042EF6C push ecx
0042EF6D lea edi,[ebp-0D4h]
0042EF73 mov ecx,35h
0042EF78 mov eax,0CCCCCCCCh
0042EF7D rep stos dword ptr es:[edi]
0042EF7F pop ecx 取出this参数
0042EF80 mov dword ptr [ebp-8],ecx
0042EF83 mov ecx,dword ptr [this]
0042EF86 call C::clone (42CD43h) 调用函数生成C对象,返回地址在eax中
0042EF8B mov dword ptr [ebp-0D0h],eax 这是clone出得C的地址
0042EF91 cmp dword ptr [ebp-0D0h],0
0042EF98 je C::clone+4Bh (42EFABh)
0042EF9A mov eax,dword ptr [ebp-0D0h]
0042EFA0 add eax,8 关键点,对返回值进行调整
0042EFA3 mov dword ptr [ebp-0D4h],eax
0042EFA9 jmp C::clone+55h (42EFB5h)
0042EFAB mov dword ptr [ebp-0D4h],0
0042EFB5 mov eax,dword ptr [ebp-0D4h]
0042EFBB pop edi
0042EFBC pop esi
0042EFBD pop ebx
0042EFBE add esp,0D4h
0042EFC4 cmp ebp,esp
0042EFC6 call @ILT+3560(__RTC_CheckEsp) (42CDEDh)
0042EFCB mov esp,ebp
0042EFCD pop ebp
0042EFCE ret
ok,通过这两步调整,函数成功调用。