虚函数虚继承内存模型

 

在C++中,涉及类的继承等较为复杂的概念,本文简单阐述一下涉及虚函数,虚继承的类的内存布局。

内存对其原则1

  1. 内置类型数据成员:结构(struct/class)的内置类型数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的起始位置要从自身大小的整数倍开始存储
  2. 结构体作为成员: 如果一个结构里有某些结构体成员,则结构体成员要从其内部“最宽基本类型成员”的整数倍地址开始存储(如struct a里存有struct b,b里有char, int, double等元素,那b应该从8的整数倍位置开始存储)。
  3. 收尾工作: 结构体的总大小,也就是sizeof的结果必须要对齐到内部”最宽基本类型成员”的整数倍,不足的要补齐。(基本类型不包括struct/class/union)。
  4. sizeof(union) 以结构里面size最大的元素为union的大小,因为在某一时刻,union只有一个成员真正存储于该地址。
就是上面的整数倍的要求,导致需要进行对齐。

作用

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:经过内存对齐后,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()   |
                                                   +--------------+
  1. 可以看到此时 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()

单链继承

img_v2_f764135d-3cf6-4f30-86ed-87be3cbdf13g

类 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

img_v2_315c777c-9672-4b4c-9ca1-765f0a14606g

此时由于 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()   |
                                                +--------------------+
  1. offset_to_top 表示的就是动态类型起始地址相对于静态类型起始地址的偏移量。因此在向下动态转换到动态类型时,让this指针加上这个偏移量即可得到动态类型的地址。

  2. 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--------+
                                          +---------------------+     
  1. 首先 B C 是独立存在的,需要两个 vptr 分别对虚函数进行索引
  2. 另外由于虚继承于 A,只有一份拷贝,

参考资料

  1. C++ 内存对齐原则及作用
  2. C++中虚函数、虚继承内存模型
  3. c++类的大小计算
  4. C++(虚)继承类的内存占用大小
  5. 虚基类指针vbptr和虚函数指针vfptr
  6. Understanding the size of virtual derived class
  7. understanding vptr in multiple inheritance?