C++多重继承下的指针类型转换 图解

在C++中,指针的类型转换是经常发生的事情,比如将派生类指针转换为基类指针,将基类指针转换为派生类指针。指针的本质其实就是一个整数,用以记录进程虚拟内存空间中的地址编号,而指针的类型决定了编译器对其指向的内存空间的解释方式。

基于上面的理解,我们似乎可以得出一个结论,C++中对指针进行类型转换,不会改变指针的值,只会改变指针的类型(即改变编译器对该指针指向内存的解释方式),但是这个结论在C++多重继承下是 不成立的。

看下面一段代码:

Copy to Clipboard Liehuo.Net Codes引用的内容: [www.veryhuo.com]
#include <iostream> 
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#中被摒弃,因为多重继承在类型转换中会出现一些有意思的现象。

下面是虚方法多重继承

[cpp]  view plain copy
  1. #include <iostream>  
  2. #define interface struct  
  3.   
  4. using namespace std;  
  5.   
  6. interface IA {  
  7.     virtual void FA() = 0;  
  8. };  
  9.   
  10. interface IB {  
  11.     virtual void FB() = 0;  
  12. };  
  13.   
  14. class CA:  
  15.     public IA,public IB  
  16. {  
  17.     virtual void FA()  
  18.     {  
  19.         cout<<"FA"<<endl;  
  20.     }  
  21.     virtual void FB()  
  22.     {  
  23.         cout<<"FB"<<endl;  
  24.     }  
  25. };  
  26.   
  27. int main()  
  28. {  
  29.     CA* pCA = new CA();  
  30.     IA* pIA = static_cast<IA*>(pCA);  
  31.     IB* pIB = static_cast<IB*>(pCA);  
  32.     return 0;  
  33. }  

在程序中加入断点,调试运行,发现pIA的值为0x003a53f8,

而pIB的值为0x003a53fc,也就是说pIB比pIA向后偏移了4个字节

派生类对象指针转换为不同基类对象指针时,编译器会做一些小小的手脚

按照派生类声明的继承顺序,转换为第一基类时指针不变,以后依次向后偏移前一基类所占字节数

这里第一基类IA没有数据成员,偏移量是虚表指针的大小-4字节,

如果有数据成员,还要加上数据成员的大小

注:基于数据对齐原则,在32位系统不足4字节算做4字节

为什么会这样呢?

对于虚方法对象来说,对象开始位置是虚表指针,如果多继承的话

每一个基类都有各自的虚表指针和数据成员,这样派生类对象如何构造呢?

派生类的内存结构如下:

基类A虚表指针 基类A数据成员 基类B虚表指针 基类B数据成员 。。。。。。 派生类数据成员
这样,派生类对象指针在向上转型时,要正确的获取基类的虚方法和数据成员,

只有将指针改变。



C++多重继承中对函数指针的调整--语言扩充之调整


代码如下:

[cpp]  view plain copy
  1. class A{  
  2. public:  
  3.     A(){_a = 0x00;}  
  4.     virtual ~A(){}  
  5.     virtual void printA(){}  
  6.     virtual A* clone()const{return NULL;}  
  7.     int _a;  
  8. };  
  9. class B{  
  10. public:  
  11.     B(){_b = 0x11;}  
  12.     virtual ~B(){}  
  13.     virtual void printB(){}  
  14.     virtual B* clone()const{return NULL;}  
  15.     int _b;  
  16. };  
  17. class C:public A, public B{  
  18. public:  
  19.     C(){_c = 0x22;}  
  20.     virtual ~C(){}  
  21.     virtual C* clone()const{return new C;}  
  22.     int _c;  
  23. };  
  24. int _tmain()  
  25. {  
  26.     A a;  
  27.     B b;  
  28.     C c;  
  29.     B* pb = &c;  
  30.     <span style="color:#ff0000;">B* pc = pb->clone();</span>  
  31. }  
这里的关键是clone函数,它的返回值是指向C类型对象的指针,如果要赋给B*指针的话,则返回指针应该加上8,移指向C对象中的B子对象,那这个调整是在哪完成的了?

首先看一下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,通过这两步调整,函数成功调用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值