好奇的探索者,理性的思考者,踏实的行动者。
Table of Contents:
对象所占用的存储空间的大小等于各成员变量所占用的存储空间的大小之和(如果不考虑成员变量对齐问题的话)。
对象的大小只受成员变量的影响,和成员函数没有关系。成员函数放在代码区。
和结构体非常类似,对象也会有内存对齐的问题
static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。
这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。
静态成员变量必须初始化,而且只能在类体外进行
有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,而所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。
成员变量按照派生的层级依次排列,新增成员变量始终在最后,当基类的成员变量被遮蔽时,仍然会留在派生类对象的内存中。
在派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算。
A类的m_b 和 B类的m_c,被C类给遮蔽了。如果访问A类的m_b成员,得加上作用域符号 A::m_b
vtable
基类对象的排列顺序和继承时声明的顺序相同。
class C: public A, public B{ } 的示意图:
编译器在知道对象首地址的情况下,通过计算偏移来存取成员变量。对于普通继承,基类成员变量的偏移是固定的,不会随着继承层级的增加而改变,存取起来非常方便。
而对于虚继承,恰恰和普通继承相反,大部分编译器会把基类成员变量放在派生类成员变量的后面,这样随着继承层级的增加,基类成员变量的偏移就会改变,就得通过其他方案来计算偏移量。
虚继承时的派生类对象被分成了两部分:
* 偏移量不会随着继承层次的增加而改变,称为固定部分;
* 偏移量会随着继承层次的增加而改变(虚继承,基类成员变量放在派生类成员变量的后面),称为共享部分。共享部分需要只保存一份,故放到最后合适。如:a -> b,c ->d, 其中虚继承的a就放到最后
当要访问对象的成员变量时,需要知道对象的首地址和变量的偏移,对象的首地址很好获得,关键是变量的偏移。
对于固定部分,偏移是不变的,很好计算;
而对于共享部分,偏移会随着继承层次的增加而改变,这就需要设计一种方案,在偏移不断变化的过程中准确地计算偏移。
各个编译器正是在设计这一方案时出现了分歧,不同的编译器设计了不同的方案来计算共享部分的偏移。
对于虚继承,将派生类分为固定部分和共享部分,并把共享部分放在最后,几乎所有的编译器都在这一点上达成了共识。主要的分歧就是如何计算共享部分的偏移,可谓是百花齐放,没有统一标准。
共享部分放在最后原因?
虚基类的成员的处理方式不同,比如菱形继承只有一份会继承,那如何单独处理只保留一份虚继承的数据呢,放在对象之后,编译器就会根据后面的虚继承的成员从而去重,以达到只保留一份虚基类的数据。
假设 A 是 B 的虚基类,B 又是 C 的虚基类,那么各个对象的内存模型如下图所示:
派生类对象中安插各个虚基类指针
早期的 cfront 编译器会在派生类对象中安插一些指针,每个指针指向一个虚基类的子对象,要存取继承来的成员变量,可以使用指针间接完成。
其实质是增加一个指向虚基类的指针作为虚派生类的一个成员。
1. A 是 B 的虚基类
2. A 是 B 的虚基类,同时 B 也是 C 的虚基类
3. 假设 A、B、C、D 类的继承关系为
内存模型为:
缺点:
* 随着虚继承层次的增加,访问顶层基类需要的间接转换会越来越多,效率越来越低。
* 当有多个虚基类时,派生类要为每个虚基类都安插一个指针,会增加对象的体积。
类似虚函数表,增加虚基类表,表中是虚基类的指针,派生类对象中安插指向虚基类表的指针。
虚基类表可以和vtable合并在一起,以减少成本。
VC 引入了虚基类表,如果某个派生类有一个或多个虚基类,编译器就会在派生类对象中安插一个指针,指向虚基类表。虚基类表其实就是一个数组,数组中的元素存放的是各个虚基类的偏移。
其实质是增加一个指向虚基类的指针的数组(虚基类表),然后派生类再指向这个虚基类表。
1. A 是 B 的虚基类
2. A 是 B 的虚基类,同时 B 又是 C 的虚基类
3. A、B、C、D 类的继承关系为
内存模型为: