AI智能
改变未来

[CPP] 类的内存布局

本文讨论的是下面 3 个问题:

  • 以不同方式继承之后,类的成员变量是如何分布的?
  • 虚函数表及虚函数表指针,在可执行文件中的位置?
  • 单一继承、多继承、虚拟继承之后,类的虚函数表的内容是如何变化的?

在这里涉及的变量有:有无继承、有无虚函数、是否多继承、是否虚继承。

准备工作

在开始探索类的内存布局之前,我们先了解虚函数表的概念,字节对齐的规则,以及如何打印一个类的内存布局。

查看类的内存布局

我们可以使用

clang++

来查看类的内存布局:

# 查看对象布局, 要求 main 中有 sizeof(class_t)clang++ -Xclang -fdump-record-layouts xxx.cpp# 查看虚函数表布局, 要求 main 中实例化一个对象clang++ -Xclang -fdump-record-layouts xxx.cpp# 或者clang -cc1 -fdump-vtable-layouts -emit-llvm xxx.cpp

虚函数表

  • 每个类都有一个属于自己虚函数表,虚函数表属于类,而不是某一个实例化对象。
  • 如果一个类声明了虚函数,那么在该类的所有实例化对象中,在
    [0, 7]

    这 8 个字节(假设是 64 位机器),会存放一个虚函数表的指针

    vtable

  • 虚函数表中的每一个元素都是一个函数地址,指向代码段的某一虚函数。
  • 虚函数表指针
    vtable

    是在对象实例化的时候填入的(因此构造函数不能用

    virtual

    声明为一个虚函数)。假设 B 继承了 A ,假如我们在运行时有

    A *a = new B()

    ,那么

    a->vtable

    实际上填入的是类 B 的虚函数表地址。

  • 如何获得
    vtable

    的值?通过读取对象的起始 8 个字节的内容,即

    *(uint64_t *)&object

+---------+                                                   +----------------+| entity1 |                                                   | .text segment  |+---------+                                                   +----------------+| vtable  |-------+                                  +------->| Entity::vfunc1 || member1 |       |         +-----------------+      |  +---->| Entity::vfunc2 || member2 |       |         | Entity\'s vtable |      |  |     |       ...      |+---------+       |         +-----------------+      |  |     +----------------++-------->| 0 : vfunc_ptr0  |------+  |     | Entity::func1  |+---------+       |         | 1 : vfunc_ptr1  |---------+     | Entity::func2  || entity2 |       |         |      ...        |               |       ...      |+---------+       |         +-----------------+               +----------------+| vtable  |-------+| member1 || member2 |+---------+

那么虚函数表(即上图的

Entity\'s vtable

)会存放在哪里呢?

一个直觉是与

static

成员变量一样,存放在

.data segment

,因为二者都属于是类共享的数据。

字节对齐

字节对齐的规则:按照编译器「已经扫描」的最长的数据类型的字节数 (总是为

1, 2, 4, 8

) 进行对齐,并且尽量填满「空隙」。

编译器是按照声明顺序(从前往后扫描)来解析一个

struct / class

的。

需要注意的是,不同的编译器,其字节对齐的规则会略有差异,但总的来说是大同小异的。本文所使用的编译器均为 clang/clang++ 。

例子一

struct Entity{char c1;int val;};// sizeof(Entity) = 8
  • 如果把
    char c1

    换成

    short val0

    ,那么还是 8 。

  • 如果把
    int val

    换成

    double d

    ,那么是 16 。

例子二

struct Entity{char cval;short ival;double dval;};/**** Dumping AST Record Layout0 | struct Entity0 |   char cval2 |   short ival8 |   double dval| [sizeof=16, dsize=16, align=8,|  nvsize=16, nvalign=8]*/
  • 如果
    short ival

    换成

    int ival

    ,那么

    ival

    的起始位置是 4 (因为编译器扫描到

    ival

    的时候,看到的最长字节数是

    sizeof(int) = 4

    )。

例子三

struct Entity{char cval;double dval;char cval2;int ival;};/**** Dumping AST Record Layout0 | struct Entity0 |   char cval8 |   double dval16 |   char cval220 |   int ival| [sizeof=24, dsize=24, align=8,|  nvsize=24, nvalign=8]*/

此处的例子,就是为了说明上述的「尽可能填满空隙」,注意到

cval2

ival

之间留出了

17, 18, 19

这 3 个字节的空白。

  • cval2, ival

    插入任意的一个字节的数据类型(最多插入 3 个),不会影响

    sizeof(Entity)

    的大小。

  • 如果我们在
    cval2, ival

    之间插入一个

    short sval

    ,那么

    sval

    会位于 18 这一位置。

例子四

如果有虚函数,又会怎么样呢?

class Entity{char cval;virtual void vfunc() {}};/**** Dumping AST Record Layout0 | class Entity0 |   (Entity vtable pointer)8 |   char cval| [sizeof=16, dsize=9, align=8,|  nvsize=9, nvalign=8]*/

在 64 位机器上,一个指针的大小是 8 字节,所以编译器会按照 8 字节对齐。

单一的类

成员变量

考虑无虚函数的条件下,成员变量的内存布局。

class A{private:short val1;public:int val2;double d;static char ch;void funcA1() {}};int main(){__attribute__((unused)) int k = sizeof(A);}// clang++ -Xclang -fdump-record-layouts test.cpp

使用上述命令编译之后,输出为:

*** Dumping AST Record Layout0 | class A0 |   short val14 |   int val28 |   double d| [sizeof=16, dsize=16, align=8,|  nvsize=16, nvalign=8]

从上面的输出可以看出:

  • static

    类型的成员并不占用实例化对象的内存(因为

    static

    类型的成员存放在静态数据区

    .data

    )。

  • 成员函数不占用内存(因为存放在代码段
    .text

    )。

  • 成员变量的权限级别
    private, public

    不影响内存布局,内存布局只跟声明顺序有关(可能需要字节对齐)。

虚函数表

class A{private:short val1;public:int val2;double d;static char ch;void funcA1() {}virtual void vfuncA1() {}virtual void vfuncA2() {}};int main(){__attribute__((unused)) int k = sizeof(A);// __attribute__((unused)) A a;}

内存布局如下:

clang++ -Xclang -fdump-record-layouts test.cpp*** Dumping AST Record Layout0 | class A0 |   (A vtable pointer)8 |   short val112 |   int val216 |   double d| [sizeof=24, dsize=24, align=8,|  nvsize=24, nvalign=8]clang++ -Xclang -fdump-vtable-layouts test.cppOriginal mapVtable for \'A\' (4 entries).0 | offset_to_top (0)1 | A RTTI-- (A, 0) vtable address --2 | void A::vfuncA1()3 | void A::vfuncA2()VTable indices for \'A\' (2 entries).0 | void A::vfuncA1()1 | void A::vfuncA2()

从这里可以看出,虚函数表的指针默认是存放在一个类的起始位置(一般占用 4 或者 8 字节,视乎机器的字长)。

  • offset_to_top(0)

    : 表示当前这个虚函数表地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为0。如果是多继承的情况,一个类可能存在多个

    vtable

    的指针。

  • RTTI

    : 即 Run Time Type Info, 指向存储运行时类型信息 (

    type_info

    ) 的地址,用于运行时类型识别,用于

    typeid

    dynamic_cast

单一继承

成员变量

class A{public:char aval;static int sival;void funcA1();};class B : public A{public:double bval;void funcB1();};class C : public B{public:int cval;void funcC1() {}};

内存布局:

clang++ -Xclang -fdump-record-layouts test.cpp*** Dumping AST Record Layout0 | class A0 |   char aval| [sizeof=1, dsize=1, align=1,|  nvsize=1, nvalign=1]*** Dumping AST Record Layout0 | class B0 |   class A (base)0 |     char aval8 |   double bval| [sizeof=16, dsize=16, align=8,|  nvsize=16, nvalign=8]*** Dumping AST Record Layout0 | class C0 |   class B (base)0 |     class A (base)0 |       char aval8 |     double bval16 |   int cval| [sizeof=24, dsize=20, align=8,|  nvsize=20, nvalign=8]

可以看出,普通的单一继承,成员变量是从上到下依次排列的,并且遵循前面提到的字节对齐规则。

虚函数表

  • A 中有 2 个虚函数
    vfuncA1, vfuncA2

    .

  • B 重写 (Override) 了
    vfuncA1

    ,自定义虚函数

    vfuncB

    .

  • C 重写了
    vfunc1

    ,自定义虚函数

    vfuncC

    .

class A{public:char aval;static int sival;virtual void vfuncA1() {}virtual void vfuncA2() {}};class B : public A{public:double bval;virtual void vfuncA1() {}virtual void vfuncB() {}};class C : public B{public:int cval;virtual void vfuncA1() {}virtual void vfuncC() {}};

成员变量布局:

clang++ -Xclang -fdump-record-layouts test.cpp*** Dumping AST Record Layout0 | class A0 |   (A vtable pointer)8 |   char aval| [sizeof=16, dsize=9, align=8,|  nvsize=9, nvalign=8]*** Dumping AST Record Layout0 | class B0 |   class A (primary base)0 |     (A vtable pointer)8 |     char aval16 |   double bval| [sizeof=24, dsize=24, align=8,|  nvsize=24, nvalign=8]*** Dumping AST Record Layout0 | class C0 |   class B (primary base)0 |     class A (primary base)0 |       (A vtable pointer)8 |       char aval16 |     double bval24 |   int cval| [sizeof=32, dsize=28, align=8,|  nvsize=28, nvalign=8]

3 个类的虚函数表如下:

clang++ -Xclang -fdump-vtable-layouts test.cppOriginal mapvoid C::vfuncA1() -> void B::vfuncA1()void B::vfuncA1() -> void A::vfuncA1()Vtable for \'C\' (6 entries).0 | offset_to_top (0)1 | C RTTI-- (A, 0) vtable address ---- (B, 0) vtable address ---- (C, 0) vtable address --2 | void C::vfuncA1()3 | void A::vfuncA2()4 | void B::vfuncB()5 | void C::vfuncC()VTable indices for \'C\' (2 entries).0 | void C::vfuncA1()3 | void C::vfuncC()Original mapvoid C::vfuncA1() -> void B::vfuncA1()void B::vfuncA1() -> void A::vfuncA1()Vtable for \'B\' (5 entries).0 | offset_to_top (0)1 | B RTTI-- (A, 0) vtable address ---- (B, 0) vtable address --2 | void B::vfuncA1()3 | void A::vfuncA2()4 | void B::vfuncB()VTable indices for \'B\' (2 entries).0 | void B::vfuncA1()2 | void B::vfuncB()Original mapvoid C::vfuncA1() -> void B::vfuncA1()void B::vfuncA1() -> void A::vfuncA1()Vtable for \'A\' (4 entries).0 | offset_to_top (0)1 | A RTTI-- (A, 0) vtable address --2 | void A::vfuncA1()3 | void A::vfuncA2()VTable indices for \'A\' (2 entries).0 | void A::vfuncA1()1 | void A::vfuncA2()

可以看出,在单一继承中,子类的虚函数表通过以下步骤构造出来:

  • 先拷贝上一层次父类的虚函数表。
  • 如果子类有自定义虚函数(例如
    B::vfuncB, C::vfuncC

    ),那么直接在虚函数表后追加这些虚函数的地址。

  • 如果子类覆盖了父类的虚函数,使用新地址(例如
    B::vfuncA1, C::vfuncA1

    )覆盖原有地址(即

    A::vfunc1

    )。

多继承

现直接组合成员变量和虚函数一起来看。

class A{char aval;virtual void vfuncA1() {}virtual void vfuncA2() {}};class B{double bval;virtual void vfuncB1() {}virtual void vfuncB2() {}};class C : public A, public B{char cval;virtual void vfuncC() {}virtual void vfuncA1() {}virtual void vfuncB1() {}};

内存布局如下(注意类 C 的布局):

clang++ -Xclang -fdump-record-layouts test.cpp*** Dumping AST Record Layout0 | class A0 |   (A vtable pointer)8 |   char aval| [sizeof=16, dsize=9, align=8,|  nvsize=9, nvalign=8]*** Dumping AST Record Layout0 | class B0 |   (B vtable pointer)8 |   double bval| [sizeof=16, dsize=16, align=8,|  nvsize=16, nvalign=8]*** Dumping AST Record Layout0 | class C0 |   class A (primary base)0 |     (A vtable pointer)8 |     char aval16 |   class B (base)16 |     (B vtable pointer)24 |     double bval32 |   char cval| [sizeof=40, dsize=33, align=8,|  nvsize=33, nvalign=8]

注意到类 C 的内存布局:

  • 一共 40 字节,有 2 个
    vtable

    指针。

  • 继承有
    primary base

    父类和普通

    base

    父类之分。

实际上就是:

+--------+--------+---------------+| offset |  size  |   content     |+--------+--------+---------------+|   0    |   8    | vtable1       ||   8    |   1    | aval          ||   9    |   7    | aligned bytes ||   16   |   8    | vtable2       ||   24   |   8    | bval          ||   32   |   1    | cval          ||   33   |   7    | aligned bytes |+--------+--------+---------------+

总的来说,在最底层子类的内存布局中,多继承的成员变量,以及

vtable

指针的排列规则是:

  • 第一个声明的继承是
    primary base

    父类。

  • 按照继承的声明顺序依次排列,并需要遵循编译器的字节对齐规则。
  • 最后排列最底层子类的成员变量。

虚函数表如下(省略了 A 和 B 的内容):

clang++ -Xclang -fdump-vtable-layouts test.cppOriginal mapvoid C::vfuncA1() -> void A::vfuncA1()Vtable for \'C\' (10 entries).0 | offset_to_top (0)1 | C RTTI-- (A, 0) vtable address ---- (C, 0) vtable address --2 | void C::vfuncA1()3 | void A::vfuncA2()4 | void C::vfuncC()5 | void C::vfuncB1()6 | offset_to_top (-16)7 | C RTTI-- (B, 16) vtable address --8 | void C::vfuncB1()[this adjustment: -16 non-virtual] method: void B::vfuncB1()9 | void B::vfuncB2()Thunks for \'void C::vfuncB1()\' (1 entry).0 | this adjustment: -16 non-virtualVTable indices for \'C\' (3 entries).0 | void C::vfuncA1()2 | void C::vfuncC()3 | void C::vfuncB1()

从上面可以看出,C 的虚函数表是由 2 部分组成的:

  • 首先是 「C 继承 A」,按照上述单一继承的虚函数表生成原则,生成了第一个虚函数表。此时
    C::vfuncB1()

    对于 A 来说是一个自定义的虚函数,因此虚函数表的第一部分有 4 个函数地址。

  • 其次是「C 继承 B」,同样按照单一继承的规则生成,但不用追加
    C::vfuncC()

    ,因为

    C::vfuncC()

    已经在第一部分填入。

可以发现的是:

  • C 的虚函数表存在一个重复的函数地址
    C::vfuncB1

  • 虽然 C 有 2 个
    vtable

    指针,但仍然只有一个虚函数表( 😅 其实也可以理解为 2 个表,不过这 2 个表是紧挨着的),而 2 个

    vtable

    指针指向了虚函数表的不同位置(也许跟编译器的处理有关,至少 clang 下的情况是这样的)。

加入虚函数表后,C 的内存布局如下:

+-----------------------+|-2: offset_to_top(0)  ||-1: C RTTI            |+--------+--------+---------------+                      +-----------------------+| offset |  size  |   content     |                      | class C\'s vtable      |+--------+--------+---------------+                      +-----------------------+|   0    |   8    | vtable1       |--------------------->| 0: C::vfuncA1_ptr     ||   8    |   1    | aval          |                      | 1: A::vfuncA2_ptr     ||   9    |   7    | aligned bytes |                      | 2: C::vfuncC_ptr      ||   16   |   8    | vtable2       |------------+         | 3: C::vfuncB1_ptr     ||   24   |   8    | bval          |            |         | 4: offset_to_top(-16) ||   32   |   1    | cval          |            |         | 5: C RTTI             ||   33   |   7    | aligned bytes |            +-------->| 6: C::vfuncB1_ptr     |+--------+--------+---------------+                      | 7: B::vfuncB2_ptr     |+-----------------------+

如何验证这个想法呢?

class A{public:char aval;virtual void vfuncA1() { cout << "A::vfuncA1()" << endl; }virtual void vfuncA2() { cout << "A::vfuncA2()" << endl; }};class B{public:double bval;virtual void vfuncB1() { cout << "B::vfuncB1()" << endl; }virtual void vfuncB2() { cout << "B::vfuncB2()" << endl; }};class C : public A, public B{public:char cval;virtual void vfuncC() { cout << "C::vfuncC()" << endl; }virtual void vfuncA1() { cout << "C::vfuncA1()" << endl; }virtual void vfuncB1() { cout << "C::vfuncB1()" << endl; }};int main(){__attribute__((unused)) int k = sizeof(C);C c;uint64_t *cvtable = (uint64_t *)*(uint64_t *)(&c);uint64_t *cvtable2 = (uint64_t *)*(uint64_t *)((uint8_t *)(&c) + 16);typedef void (*func_t)(void);cout << "---- vtable1 ----" << endl;((func_t)(*(cvtable + 0)))();    // C::vfuncA1()((func_t)(*(cvtable + 1)))();    // A::vfuncA2()((func_t)(*(cvtable + 2)))();    // C::vfuncC()((func_t)(*(cvtable + 3)))();    // C::vfuncB1()printf("offset_to_top = %d\\n", *(cvtable2 - 2));  // -16cout << "---- vtable2 ----" << endl;((func_t)(*(cvtable2 + 0)))();   // C::vfuncB1(), same as cvtable + 6((func_t)(*(cvtable2 + 1)))();   // B::vfuncB2(), same as cvtable + 7}

棱形继承和虚拟继承

如果我们需要用到类似「棱形」的继承链,那么就要通过「虚拟继承」的方式实现。

假设此处的继承链为:

Base/     \\A       B\\   /Child

如果不使用

virtual

修饰继承方式:

class Base { public: int value; };class A : public Base { };class B : public Base { };class Child : public A, public B { };int main(){Child child;child.value;}

那么成员变量

child.value

会出现编译时错误 (clang++) ,类似于「命名冲突」。

单一虚拟继承

class Base{char baseval;virtual void vfuncBase1() {}virtual void vfuncBase2() {}};class A : virtual public Base{double aval;virtual void vfuncBase1() {}virtual void vfuncA() {}};class B : virtual public Base{double bval;virtual void vfuncBase2() {}virtual void vfuncB() {}};

以 A 为例子进行说明。成员变量布局:

clang++ -Xclang -fdump-record-layouts diamond2.cpp*** Dumping AST Record Layout0 | class A0 |   (A vtable pointer)8 |   double aval16 |   class Base (virtual base)16 |     (Base vtable pointer)24 |     char baseval| [sizeof=32, dsize=25, align=8,|  nvsize=16, nvalign=8]

与上述的「单一继承」不同,此处虚拟继承是会有 2 个

vtable

指针的,并且被虚拟继承的目标(即

Base

会排列在最后面)。

虚函数表的内容如下:

clang++ -Xclang -fdump-vtable-layouts diamond2.cppOriginal mapVtable for \'A\' (11 entries).0 | vbase_offset (16)1 | offset_to_top (0)2 | A RTTI-- (A, 0) vtable address --3 | void A::vfuncBase1()4 | void A::vfuncA()5 | vcall_offset (0)6 | vcall_offset (-16)7 | offset_to_top (-16)8 | A RTTI-- (Base, 16) vtable address --9 | void A::vfuncBase1()[this adjustment: 0 non-virtual, -24 vcall offset offset] method: void Base::vfuncBase1()10 | void Base::vfuncBase2()Virtual base offset offsets for \'A\' (1 entry).Base | -24Thunks for \'void A::vfuncBase1()\' (1 entry).0 | this adjustment: 0 non-virtual, -24 vcall offset offsetVTable indices for \'A\' (2 entries).0 | void A::vfuncBase1()1 | void A::vfuncA()

化简一下:

A vtable:                      B vtable:- A::vfuncBase1()              - B::vfuncBase2()- A::vfuncA()                  - B::vfuncB()- A::vfuncBase1()              - Base::vfuncBase1()- Base::vfuncBase2()           - B::vfuncBase2()

从上面可以看出:

  • 虚函数表的第一部分
    3-4

    ,按照

    A

    是一个「单一的类」时的规则构造。

  • 虚函数表的第二部分
    9-10

    ,按照

    A

    单一继承

    Base

    的规则构造。

棱形继承的成员变量

class Child : public A, public B{char childval;virtual void vfuncC() {}virtual void vfuncB() {}virtual void vfuncA() {}};

Child

成员变量内存布局如下:

clang++ -Xclang -fdump-record-layouts diamond.cpp*** Dumping AST Record Layout0 | class A0 |   (A vtable pointer)8 |   double aval16 |   class Base (virtual base)16 |     char baseval| [sizeof=24, dsize=17, align=8,|  nvsize=16, nvalign=8]*** Dumping AST Record Layout0 | class B0 |   (B vtable pointer)8 |   double bval16 |   class Base (virtual base)16 |     char baseval| [sizeof=24, dsize=17, align=8,|  nvsize=16, nvalign=8]*** Dumping AST Record Layout0 | class Child0 |   class A (primary base)0 |     (A vtable pointer)8 |     double aval16 |   class B (base)16 |     (B vtable pointer)24 |     double bval32 |   char childval33 |   class Base (virtual base)33 |     char baseval| [sizeof=40, dsize=34, align=8,|  nvsize=33, nvalign=8]

Child

中:

  • 成员变量和虚函数指针与「多继承」的情况相同。
  • Child

    Base

    (被虚拟继承的父类)的内容排在最后(比

    Child

    的自定义成员还要后),并且只保留了一份

    Base

    的数据,这就是虚拟继承的作用。

棱形继承的虚函数表

A, B

的虚函数表,如「单一虚拟继承」一节所述。

Child

的虚函数表如下:

clang++ -Xclang -fdump-vtable-layouts diamond.cppOriginal mapvoid Child::vfuncA() -> void A::vfuncA()Vtable for \'Child\' (18 entries).0 | vbase_offset (40)1 | offset_to_top (0)2 | Child RTTI-- (A, 0) vtable address ---- (Child, 0) vtable address --3 | void A::vfuncBase1()4 | void Child::vfuncA()5 | void Child::vfuncC()6 | void Child::vfuncB()7 | vbase_offset (24)8 | offset_to_top (-16)9 | Child RTTI-- (B, 16) vtable address --10 | void B::vfuncBase2()11 | void Child::vfuncB()[this adjustment: -16 non-virtual] method: void B::vfuncB()12 | vcall_offset (-24)13 | vcall_offset (-40)14 | offset_to_top (-40)15 | Child RTTI-- (Base, 40) vtable address --16 | void A::vfuncBase1()[this adjustment: 0 non-virtual, -24 vcall offset offset] method: void Base::vfuncBase1()17 | void B::vfuncBase2()[this adjustment: 0 non-virtual, -32 vcall offset offset] method: void Base::vfuncBase2()Virtual base offset offsets for \'Child\' (1 entry).Base | -24Thunks for \'void Child::vfuncB()\' (1 entry).0 | this adjustment: -16 non-virtualVTable indices for \'Child\' (3 entries).1 | void Child::vfuncA()2 | void Child::vfuncC()3 | void Child::vfuncB()

回顾一下 A 和 B 的虚函数表:

A vtable:                      B vtable:- A::vfuncBase1()              - B::vfuncBase2()- A::vfuncA()                  - B::vfuncB()- A::vfuncBase1()              - Base::vfuncBase1()- Base::vfuncBase2()           - B::vfuncBase2()

可以看出,

Child

的虚函数表有 2 部分:

  • 第一部分
    3-6, 10-11

    ,与

    Child

    多继承

    A, B

    的构造规则类似,即合并

    Avtable[0 - 1]

    Bvtable[0 - 1]

  • 第二部分
    16-17

    ,合并

    Avtable[2 - 3]

    Bvtable[2 - 3]

总结

场景 成员变量 虚函数表
单一的类 按照声明顺序依次排列,并需要遵循字节对齐的规则 在对象的起始 8 个字节的内存中,存放

vtable

指针

单一继承 1. 按照继承的层次顺序,依次排列,并需要遵循字节对齐的规则
2. 只有一个

vtable

指针

1. 拷贝上一层次父类的虚函数表
2. 如果有自定义的虚函数,在虚函数表后追加对应的地址
3. 如果 Override 了父类虚函数,那么使用新地址覆盖原有地址。
多继承 1. 多个

vtable

指针
2. 按照继承的顺序,依次排列父类的

<vtable, members>
参考「多继承」一节。
单一虚拟继承 与普通的单一继承不同,会有多个

vtable

指针

2 部分:第一部分按照「单一的类」规则和第二部分按照「单一继承」规则。
棱形继承 1. 与多继承类似
2. 在最后添加被虚拟继承目标的数据
参考「棱形继承的虚函数表」一节。
赞(0) 打赏
未经允许不得转载:爱站程序员基地 » [CPP] 类的内存布局