Table of Contents:

 单个对象的内存模型

对象所占用的存储空间的大小等于各成员变量所占用的存储空间的大小之和(如果不考虑成员变量对齐问题的话)。
对象的大小只受成员变量的影响,和成员函数没有关系。成员函数放在代码区。
和结构体非常类似,对象也会有内存对齐的问题
static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放
这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。
静态成员变量必须初始化,而且只能在类体外进行

 继承时的对象内存模型

有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,而所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。
成员变量按照派生的层级依次排列,新增成员变量始终在最后,当基类的成员变量被遮蔽时,仍然会留在派生类对象的内存中。
在派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算。
A类的m_b 和 B类的m_c,被C类给遮蔽了。如果访问A类的m_b成员,得加上作用域符号  A::m_b

 多继承时的对象内存模型

基类对象的排列顺序和继承时声明的顺序相同。
class C: public A, public B{ } 的示意图:

 C++虚继承下的内存模型

编译器在知道对象首地址的情况下,通过计算偏移来存取成员变量。对于普通继承,基类成员变量的偏移是固定的,不会随着继承层级的增加而改变,存取起来非常方便。

而对于虚继承,恰恰和普通继承相反,大部分编译器会把基类成员变量放在派生类成员变量的后面,这样随着继承层级的增加,基类成员变量的偏移就会改变,就得通过其他方案来计算偏移量。

虚继承时的派生类对象被分成了两部分:
 偏移量不会随着继承层次的增加而改变,称为固定部分
 偏移量会随着继承层次的增加而改变(虚继承,基类成员变量放在派生类成员变量的后面),称为共享部分

当要访问对象的成员变量时,需要知道对象的首地址和变量的偏移,对象的首地址很好获得,关键是变量的偏移。
对于固定部分,偏移是不变的,很好计算;
而对于共享部分,偏移会随着继承层次的增加而改变,这就需要设计一种方案,在偏移不断变化的过程中准确地计算偏移
各个编译器正是在设计这一方案时出现了分歧,不同的编译器设计了不同的方案来计算共享部分的偏移

 对于虚继承,将派生类分为固定部分和共享部分,并把共享部分放在最后,几乎所有的编译器都在这一点上达成了共识。主要的分歧就是如何计算共享部分的偏移,可谓是百花齐放,没有统一标准。

共享部分放在最后原因?
虚基类的成员的处理方式不同,比如菱形继承只有一份会继承,那如何单独处理只保留一份虚继承的数据呢,放在对象之后,编译器就会根据后面的虚继承的成员单从而去重,以达到只保留一份虚基类的数据。

假设 A 是 B 的虚基类,B 又是 C 的虚基类,那么各个对象的内存模型如下图所示:

 1.虚基类指针方式(cfront解决方案)

早期的 cfront 编译器会在派生类对象中安插一些指针,每个指针指向一个虚基类的子对象,要存取继承来的成员变量,可以使用指针间接完成。
实质是增加一个指向虚基类的指针作为虚派生类的一个成员。
1. A 是 B 的虚基类

2. A 是 B 的虚基类,同时 B 也是 C 的虚基类

3. 假设 A、B、C、D 类的继承关系为

内存模型为:

缺点:
 随着虚继承层次的增加,访问顶层基类需要的间接转换会越来越多,效率越来越低。
 当有多个虚基类时,派生类要为每个虚基类都安插一个指针,会增加对象的体积。

 2.虚基类表方式(VC解决方案)

VC 引入了虚基类表,如果某个派生类有一个或多个虚基类,编译器就会在派生类对象中安插一个指针,指向虚基类表。虚基类表其实就是一个数组,数组中的元素存放的是各个虚基类的偏移。
实质是增加一个指向虚基类的指针的数组(虚基类表),然后派生类再指向这个虚基类表。
1. A 是 B 的虚基类

2. A 是 B 的虚基类,同时 B 又是 C 的虚基类

3.  A、B、C、D 类的继承关系为

内存模型为:

  多态下对象内存模型

 如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这里的数组就是虚函数表(Virtual function table),简写为vtable
 在对象的开头位置有一个指针 vfptr,指向虚函数表,并且这个指针始终位于对象的开头位置
* 基类的虚函数在 vtable 中的索引是固定的,不会随着继承层次的增加而改变,派生类新增的虚函数放在 vtable 的最后。如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那么将使用派生类的虚函数替换基类的虚函数(这就是多态的原因)。