在C++中,涉及类的继承等较为复杂的概念,本文简单阐述一下涉及虚函数,虚继承的类的内存布局。
内存对其原则1
- 内置类型数据成员:结构(struct/class)的内置类型数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的起始位置要从自身大小的整数倍开始存储
- 结构体作为成员: 如果一个结构里有某些结构体成员,则结构体成员要从其内部“最宽基本类型成员”的整数倍地址开始存储(如struct a里存有struct b,b里有char, int, double等元素,那b应该从8的整数倍位置开始存储)。
- 收尾工作: 结构体的总大小,也就是sizeof的结果必须要对齐到内部”最宽基本类型成员”的整数倍,不足的要补齐。(基本类型不包括struct/class/union)。
- sizeof(union) 以结构里面size最大的元素为union的大小,因为在某一时刻,union只有一个成员真正存储于该地址。
作用
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:经过内存对齐后,CPU的内存访问速度大大提升。 在程序员看来,内存是由一个个的字节组成。而CPU并不是这么看待的,CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度)。如果内存没有对齐,CPU就需要花费额外的时间寻找每个块中真正有效的字节。
==其实如下分为了这么多类别,最主要的就是区分到底需要多少个 vptr
==
并且需要注意内存模型各个 vptr、数据成员出现的先后顺序,因为这会影响 padding 的过程。
非多态继承内存模型
多态的判断标准在于是否有虚函数的存在,否则子类的同名函数会覆盖父类的同名函数,且只能根据静态类型调用相应的函数。
class AAA {
private:
int a;
void funcA() {}
};
class BBB : public AAA {
public:
void funcB() {}
private:
int c;
};
不涉及虚表的问题,比较简单
class BBB
object
0 - int a
4 - int c
sizeof(BBB): 8
多态单继承内存模型2
struct A
{
int ax; // 成员变量
virtual void f0() {};
virtual void f1() {};
};
struct B : public A
{
int bx; // 成员变量
void f0() override {}; // 重写f0
void f2() {};
};
struct A
object A VTable (不完整)
0 - vptr_A --------------------------------> +--------------+
8 - int ax | A::f0() |
sizeof(A): 16 align: 8 +--------------+
| A::f1() |
+--------------+
struct B
object
0 - struct A B VTable (不完整)
0 - vptr_A ------------------------------> +--------------+
8 - int ax | B::f0() |
12 - int bx +--------------+
sizeof(A): 16 align: 8 | A::f1() |
+--------------+
| B::f2() |
+--------------+
- 可以看到此时 B 的虚表只需要一个指针就能够通过偏移找到所有的函数。
A a;
B b;
A &a_ra = a;
A &a_rb = b;
a_ra.f0(); // call (a_ra->vptr_A + offset0) --> A::f0() # offset0 means offset equal to 0.
a_rb.f0(); // call (a_rb->vptr_A + offset0) --> B::f0()
单链继承
类 C 仅需要一个 vtable,因此只需要一个 vptr,整张虚表是根据继承的顺序线性的补充各函数指针(当然还有一些其他的信息也在虚表里) C VTable(不完整)
struct C +------------+
object | RTTI for C |
0 - struct B +-------> +------------+
0 - struct A | | C::f0() |
0 - vptr_A -------------------------+ +------------+
8 - int ax | B::f1() |
12 - int bx +------------+
16 - int cx | C::f2() |
sizeof(C): 24 align: 8 +------------+
多态多继承内存模型2
此时由于 A B 是独立存在的,他们在虚表上不再具有线性排布的关系,因此就需要两个 vptr 分别对它们虚函数进行索引
C Vtable (7 entities)
+--------------------+
struct C | offset_to_top (0) |
object +--------------------+
0 - struct A (primary base) | RTTI for C |
0 - vptr_A -----------------------------> +--------------------+
8 - int ax | C::f0() |
16 - struct B +--------------------+
16 - vptr_B ----------------------+ | C::f1() |
24 - int bx | +--------------------+
28 - int cx | | offset_to_top (-16)|
sizeof(C): 32 align: 8 | +--------------------+
| | RTTI for C |
+------> +--------------------+
| Thunk C::f1() |
+--------------------+
-
offset_to_top
表示的就是动态类型起始地址相对于静态类型起始地址的偏移量。因此在向下动态转换到动态类型时,让this
指针加上这个偏移量即可得到动态类型的地址。 -
Thunk
就是告诉当前的this
指针它需要调整到正确的位置,此例:c real_c; B& b_rc = real_c; b_rc.f1(); // 调用的函数: b_rc->vptr_B + offset_to_top(-16)
上述的模型中,对于派生类对象,它的基类相对于它的偏移量总是确定的,因此动态向下转换并不需要依赖额外的运行时信息。
而虚继承破坏了这一条件。它表示虚基类相对于派生类的偏移量可以依实际类型不同而不同,且仅有一份拷贝,这使得虚基类的偏移量在运行时才可以确定。因此,我们需要对继承了虚基类的类型的虚表进行扩充,使其包含关于虚基类偏移量的信息。
虚继承内存模型2.3.4.5.6.7
struct A
{
int ax;
virtual void f0() {}
virtual void bar() {}
};
struct B : virtual public A /****************************/
{ /* */
int bx; /* A */
void f0() override {} /* v/ \v */
}; /* / \ */
/* B C */
struct C : virtual public A /* \ / */
{ /* \ / */
int cx; /* D */
void f0() override {} /* */
}; /****************************/
struct D : public B, public C
{
int dx;
void f0() override {}
};
// D中的B、C的偏移量可以在编译时确定,而A的偏移量在运行时确定。
注意⚠️由于生成UML的工具问题,没有体现出 B C 都是虚继承于 A
D VTable
+---------------------+
| vbase_offset(32) |
+---------------------+
struct D | offset_to_top(0) |
object +---------------------+
0 - struct B (primary base) | RTTI for D |
0 - vptr_B ----------------------> +---------------------+
8 - int bx | D::f0() |
16 - struct C +---------------------+
16 - vptr_C ------------------+ | vbase_offset(16) |
24 - int cx | +---------------------+
28 - int dx # dx在内存中先于虚基类 | | offset_to_top(-16) |
32 - struct A (virtual base) | +---------------------+
32 - vptr_A --------------+ | | RTTI for D |
40 - int ax | +---> +---------------------+
sizeof(D): 48 align: 8 | | D::f0() |
# 按照对齐原则的收尾工作,最后的 | +---------------------+
# 占据内存数要是最宽数据,此例为 | | vcall_offset(0) |x--------+
# 指针(8 bytes)的整数倍,因此 | +---------------------+ |
# 对齐到 48 bytes | | vcall_offset(-32) |o----+ |
| +---------------------+ | |
| | offset_to_top(-32) | | |
| +---------------------+ | |
| | RTTI for D | | |
+--------> +---------------------+ | |
| Thunk D::f0() |o----+ |
+---------------------+ |
| A::bar() |x--------+
+---------------------+
- 首先 B C 是独立存在的,需要两个 vptr 分别对虚函数进行索引
- 另外由于虚继承于 A,只有一份拷贝,