C++继承模型的内存布局

对于多继承情况

考虑示例代码

1

2

3

struct Base1 {...};

struct Base2 {...};

struct Derived : Base1, Base2 {...};

有如下内存布局

首先出现的是派生类Derived类的虚表指针vptr

(这里插入一个提醒:

一直以来vptr都被国人翻译为虚函数表指针

但是vtbl英文原文是virtual table并非virtual function table

为什么呢

因为这个表不只是为了虚函数而准备的

一切虚拟化技术都会用这个表 包括 虚继承 RTTI等

所以当类本身与其直接间接基类内都未定义任何虚函数时也是有可能有虚表的

典型就是当前类继承了一个虚基类..提醒结束)

 

其次是第一直接基类的数据成员

然后是第二直接基类的虚表指针vptr

再次是第二直接基类的数据成员

如果还有后继直接基类 那么依此类推

也许有朋友会困惑为何偏移0处是Derived类的vptr

而不是Base1的vptr

因为非虚继承第一直接基类Base1与派生类Derived的基地址是一致的

Derived的内存结构正是从Base1开始

而对于

1

2

Base1 *pb1 = new Derived;

pb1->polymorphicFunction();

这样的多态引用

事实上我们也是用的Derived的虚表vtbl

因为Derived的vtbl和Base1的vtbl已经融为一体了

也就是说从Derived的vtbl中完全可以查到从Base1继承下来的虚函数

以及覆盖Base1的虚函数

而对于Base2来说

事实上图中所示的Base2::vptr并不是指向Base2类的vtbl

而是指向Derived类的vtbl中的一个thunk地址

这个所谓的thunk地址又指向一段汇编码

这段thunk汇编码负责做两件事:

1.跳转到vtbl中正确的虚函数(也就是Derived的虚函数)地址所在的内存单元

2.修改this指针使其指向Derived对象,并传入上一步检索到的虚函数中

 

通过这个thunk策略

编译器实现了C++的多态性

举个例子

1

2

Base2 *pb2 = new Derived;

delete pb2;

此时使用pb2调用虚析构函数时

通过Base2::vptr跳转到了thunk汇编代码

执行thunk后跳转到vtbl中Derived::~Derived()所在的槽位

然后把当前指向Base2子对象部分的的this指针

添加适当偏移使其指向Derived对象的内存首地址

然后传入并调用Derived::~Derived()

从而实现了Base2 *pb2和事实对象类Derived的动态绑定

 

由此我们不妨思考一下为何动态绑定需要用指针或引用而不能通过实例对象来实现

已知

1

2

Derived d;

Base2 b2 = d;

这里相当于拷贝了d对象中的Base2::vptr+Base2::data到新对象中

并且修改原先指向Dervied::vtbl的vptr,使其重新指向Bae2::vtbl

这可区别大了

原来使用Base2 *pb2时,Base2::vptr指针仍旧指向Dervied::vtbl中(的thunk码)

而赋值给b2对象后,Base2::vptr指向的就是一个完全不同类的虚表Base2::vtbl了

 

回到正题

最后Derived类自己定义的成员会放在所有直接基类成员的后面

但是这个说法是有待商榷的

下面的例子会进一步说明

 

对于虚继承+多继承的套路有些许变招

考虑示例代码

1

2

3

4

struct Base {...};

struct Base1 : virtual Base {...};

struct Base2 : virtual Base {...};

struct Derived : Base1, Base2 {...};

有如下内存布局

此时注意到几个变化:

 

1. 虚基类Base的内存空间位于Dervied类成员的下面

也就是位于整个内存空间的最后

即使它是所有类的共同祖先

大家不妨进一步思考这样做的目的

正常来说 非直接基类是不该出现在派生类内存中的

但编译器正是通过这样的举措

才得以实现虚基类在所有子子孙孙派生类都共享一个共同虚基类的语法特性

此时不论是把这个Derived对象赋予Base1指针还是Base2指针

他们都只需要把按照自己既定的惯例加上一个固定的偏移

就能在对象的尾部访问到那个唯一的虚基类

 

2.Derived的成员事实上是位于所有非虚直接基类的后面

位于所有继承链上的虚基类的前面

大家可以看到C++标准委员会和编译器大厂设计这一套布局的良苦用心

他们并未因为Derived::vptr在Base1的子对象域中而就盲目地把Derived::data也挪过去

而是把data放在了所有非虚直接基类的后面

这样就保证了其每个非虚直接基类的完整性和模块化

并且方便在动态绑定时计算指针偏移

例如

1

2

struct Derived: Base1,Base2,...,BaseN {...}

BaseN *pbn = new Derived;

此时只需要令

this+=sizeof(Base1+Base2+...+Base[N-1])即可指向BaseN的起始位置

非常方便 当然this是个Derived * const类型 所以不能被修改

这里只是意会转为汇编后的寄存器操作

 

3.有一个需要特别注意的地方

就是如果继承链上的某个类既没有虚指针也没有数据成员

也就是说它没有内存分配

这个时候就要小心了

其指针将指向Derived对象的首地址!

也就是该类的指针将会与pb1、pd存储同样的地址

这样做的原因就是避免它指向位于其指定内存区域下方的

一个与他无关的对象的首地址

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值