C++对象内存模型
1 虚表
2 对象内存模型
虚继承
对于一个空的类, 例如
class A { };
它会由编译器插入一个字节占位, 因此sizeof(a)会显示1.
这里本身只提供接口不提供任何数据的类称作empty virtual base class.
如果有个子类虚继承了A:
class B: public virtual A{ };
此时B的对象中, 有一个A对象占用一字节, 有个为了取得subobject的指针四字节, 还有个对齐需要3字节, 凑成8字节. 至于为什么a对象并没有对齐, 见c.org:结构体的对齐
如果编译器支持empty virtaul base class优化(即每个子类会将基类当做自己的最开头的一部分), 那么B就没有开始的1字节已经为了补足4字节整数倍的那个3字节, 因此只有四字节.
普通类
如果A定义如下:
class A{ int func(){return 0;} };
即只有函数定义而没有数据, 那么A的对象的大小仍然为1. 函数保存在进程的TEXT段.
虚继承与普通基础的对象模型(摘自陈硕博客, 有精简)
class Base : virtual public BeforeBase { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } };
普通继承
普通继承时,对象内存中基类在前,派生类在后,虚函数表指针在内存开始的地方. 因此基类与派生类共享同一个虚函数表. 在同一个虚函数表中,派生类的重名虚函数覆盖基类的相应函数派生类的新虚函数添加到虚函数表中(本质上是用来给后续派生类覆盖的)
上图中, 对象的内存开始处首先是VPTR, 然后是基类的各个成员, 再然后是继承类的成员.
虚拟继承
虚拟继承时,对象的内存分布为派生类在前,基类在后,基类的虚函数表和派生类的无法共享,各自有一张.
对两个不同的虚函数表来说, 派生类的重名虚函数覆盖基类的虚函数表中相应函数派生类的新虚函数不添加到基类的虚函数表(因为基类无需知道,新的虚函数只要后续派生类知道即可)
虚继承者(即派生者)要添加一个的虚基类表:
- VS: 在虚函数表指针后面(如果有的话),添加一个虚基类表的指针
- g++: (存疑)虚基类表的指针不额外添加,而是与虚函数表指针共用,虚函数表项往正方向递增,虚基类表项往负方向递增;(注意对于简单的虚继承情况,可能最终没有使用到虚基类表,而是被编译器优化掉了,直接取的偏移)
例子
多分支普通继承(Y型)
多分支普通继承时(包含重复继承的情况),基本原则和普通继承相同,多分支的顶点基类各有一个虚函数表,共有派生类的重名虚函数覆盖所有顶点基类的虚函数表中相应函数派生类的新虚函数仅添加到第一个顶点基类的虚函数表,无需添加到其它的
多分支虚拟继承(菱形)
基本原则和虚拟继承相同,共有虚基类仅有一份实例,虚函数覆盖和添加的原则综合了虚拟继承和多分支普通继承的情况:虚继承的派生类各有自己的虚函数表和虚基类表,虚顶点基类也有自己的虚函数表派生类的重名虚函数覆盖所有虚函数表派生类的新虚函数不添加到顶点基类的虚函数表
注意此处将D转化为B1或者B2都会有一些属于其他类的数据在内存中, 虽然最终结果由编译器计算后并不会受到影响.