vtale 内存布局分析

虚函数表指针与虚函数表布局

考虑如下的 class:

  1. class A {
  2. public:
  3. int a;
  4. virtual void f1() {}
  5. virtual void f2() {}
  6. };
  7. int main() {
  8. A *a1 = new A();
  9. return 0;
  10. }

首先明确,sizeof(A)的输出是 16,因为:class A 中含有一个 int 是 4 字节,然后含有虚函数,所以必须含有一个指向 vtable 的 vptr,而 vptr 是 8 字节,8 + 4 = 12,对齐到 8 的边界,也就是 16

上述 class 的 AST record layout 如下:

  1. *** Dumping AST Record Layout
  2. 0 | class A
  3. 0 | (A vtable pointer)
  4. 8 | int a
  5. | [sizeof=16, dsize=12, align=8,
  6. | nvsize=12, nvalign=8]

可以证明对齐边界为 8 字节

需要注意的是:由于含有指针,而 64 位系统,指针为 8 字节,所以对齐边界是 8

虚函数表指针 vptr

为了完成多态的功能,现代的 C++编译器都采用了表格驱动的对象模型,具体来说,所有虚函数的地址都存放在一个表格之中,而这个表格就被称为虚函数表vtable,这个虚函数表的地址被存在放类中,称为虚函数表指针vptr

使用 clang 导出上述 class A 的对象布局,有如下输出:

  1. *** Dumping AST Record Layout
  2. 0 | class A
  3. 0 | (A vtable pointer)
  4. 8 | int a
  5. | [sizeof=16, dsize=12, align=8,
  6. | nvsize=12, nvalign=8]

可以看到,在 class A 的对象布局中,第一个就是 vptr(8 字节)

虚函数表 vtable

利用 clang 的导出虚函数表的功能,可以看到上述 class A 的虚函数表具体内容如下:

  1. Vtable for 'A' (4 entries).
  2. 0 | offset_to_top (0)
  3. 1 | A RTTI
  4. -- (A, 0) vtable address --
  5. 2 | void A::f1()
  6. 3 | void A::f2()
  7. VTable indices for 'A' (2 entries).
  8. 0 | void A::f1()
  9. 1 | void A::f2()

需要注意的是:-- (A, 0) vtable address -- 的意思是,class A 所产生的对象的 vptr 指向的就是这个地址

我们经常所说的vtable仅仅含有虚函数的地址,实际上,这不是完整的vtable

一个完整的 vtable,有以下内容(虚函数表中的内容被称为条目或者实体,另外并不是所有的条目都会出现,但是如果出现,一定是按照下面的顺序出现):

  1. virtual call (vcall) offsets:用于对虚函数执行指针调整,这些虚函数在虚基类或虚基类的子对象中声明,并在派生自虚基类的类中重写
  2. virtual base (vbase) offsets:用来访问某个对象的虚基
  3. offset to top:记录了对象的这个虚函数表地址偏移到该对象顶部地址的偏移量
  4. typeinfo pointer:用于 RTTI
  5. vitual function pointers:一系列虚函数指针

各种情况下的 vtable 布局

1 单一继承

下面讨论,单一继承情况下,虚函数表里面各种条目的具体情况,考虑如下代码:

  1. class A {
  2. public:
  3. int a;
  4. virtual void f1() {}
  5. virtual void f2() {}
  6. };
  7. class B : public A {
  8. public:
  9. int b;
  10. void f1() override {}
  11. };
  12. int main() {
  13. A *a1 = new A();
  14. B *b1 = new B();
  15. return 0;
  16. }

首先需要明确的是:sizeof(A)与 sizeof(B)的大小:

  1. sizeof(A):4 + 8 = 12,调整到 8 的边界,所以是 16
  2. sizeof(B):4 + 4 + 8 = 16,不需要进行边界对齐,所以也是 16

利用 clang 查看 class A 与 class B 的所产生的对象 a1 与 b1 的布局,有如下输出:

  1. *** Dumping AST Record Layout
  2. 0 | class A
  3. 0 | (A vtable pointer)
  4. 8 | int a
  5. | [sizeof=16, dsize=12, align=8,
  6. | nvsize=12, nvalign=8]
  7. // 对于b1来说:在构造b1时,首先需要构造一个A父类对象,所以b1的布局最开始上半部分是一个A父类对象
  8. // 但是b1中的 vtable pointer指向的是class B的虚表
  9. *** Dumping AST Record Layout
  10. 0 | class B
  11. 0 | class A (primary base)
  12. 0 | (A vtable pointer)
  13. 8 | int a
  14. 12 | int b
  15. | [sizeof=16, dsize=16, align=8,
  16. | nvsize=16, nvalign=8]

利用 clang 查看 class A 与 class B 的虚函数表内容,有如下输出:

  1. Vtable for 'A' (4 entries).
  2. 0 | offset_to_top (0)
  3. 1 | A RTTI
  4. -- (A, 0) vtable address --
  5. 2 | void A::f1()
  6. 3 | void A::f2()
  7. VTable indices for 'A' (2 entries).
  8. 0 | void A::f1()
  9. 1 | void A::f2()
  10. Vtable for 'B' (4 entries).
  11. 0 | offset_to_top (0)
  12. 1 | B RTTI
  13. -- (A, 0) vtable address --
  14. -- (B, 0) vtable address --
  15. 2 | void B::f1()
  16. 3 | void A::f2()
  17. VTable indices for 'B' (1 entries).
  18. 0 | void B::f1()

在 class B 的虚函数表内容中,有如下两条:

-- (A, 0) vtable address --

-- (B, 0) vtable address --

意思是:

  1. 如果以 A 类型的引用或者指针来看待 class B 的对象,那么此时的 vptr 指向的就是-- (A, 0) vtable address --
  2. 如果以 B 类型的引用或者指针来看待 class B 的对象,那么此时的 vptr 指向的就是-- (B, 0) vtable address --

虽然在上述里例子中,这两个地址是相同的,这也意味着单链继承的情况下,动态向下转换和向上转换时,不需要对 this 指针的地址做出任何修改,只需要对其重新“解释”

这里需要说明一下:指针或者引用的类型,真正的意义是影响编译器如何解释或者说编译器如何看待该指针或者引用指向的内存中的数据)

此处还有另一种情况,即 class A 不含有虚函数,而 class B 含有虚函数,且 class B 继承于 class A:

  1. class A {
  2. public:
  3. int a;
  4. };
  5. class B : public A {
  6. public:
  7. int b;
  8. virtual void f1() {}
  9. };
  10. int main() {
  11. A *a1 = new A();
  12. B *b1 = new B();
  13. return 0;
  14. }

打印 class A 与 class B 的对象布局如下:

  1. *** Dumping AST Record Layout
  2. 0 | class A
  3. 0 | int a
  4. | [sizeof=4, dsize=4, align=4,
  5. | nvsize=4, nvalign=4]
  6. *** Dumping AST Record Layout
  7. 0 | class B
  8. 0 | (B vtable pointer)
  9. 8 | class A (base)
  10. 8 | int a
  11. 12 | int b
  12. | [sizeof=16, dsize=16, align=8,
  13. | nvsize=16, nvalign=8]

在这种情况下,把一个 derived class object 指定给 base class 的指针或者引用,就需要编译器的介入了(编译器需要调整地址,因为 class B object 中多了一根 vptr)

但是这种情况很少出现,因为:如果一个类要作为基类,那么它的析构函数基本上都要是虚的,否则通过指向基类的指针删除对象将会触发未定义的行为

单一继承情况下的虚函数表所含条目也比较少,理解起来也很容易

2 多重继承

考虑如下代码:

  1. class A {
  2. public:
  3. int a;
  4. virtual void f1() {}
  5. };
  6. class B {
  7. public:
  8. int b;
  9. virtual void f2() {}
  10. };
  11. class C : public A, public B {
  12. public:
  13. int c;
  14. void f1() override {}
  15. void f2() override {}
  16. };
  17. int main() {
  18. A *a1 = new A();
  19. B *b1 = new B();
  20. C *c1 = new C();
  21. return 0;
  22. }

首先,依然讨论一下 A,B,C 三个 class 的大小:

  1. sizeof(A):4 + 8 = 12,调整到 8 的边界,即 16
  2. sizeof(B):4 + 8 = 12,调整到 8 的边界,即 16
  3. sizeof(C):4 + 4 + 4 +8 + 8 = 28,调整到 8 的边界,即 32

这里有一个问题,为什么计算 C 的大小时,加了两次 8?因为这两个 8 是两个 vptr,那怎么 C 会有两根 vptr 呢,后面会进行解释,此处先不讨论

查看 class A、B、C 三个对象的布局,如下:

  1. *** Dumping AST Record Layout
  2. 0 | class A
  3. 0 | (A vtable pointer)
  4. 8 | int a
  5. | [sizeof=16, dsize=12, align=8,
  6. | nvsize=12, nvalign=8]
  7. *** Dumping AST Record Layout
  8. 0 | class B
  9. 0 | (B vtable pointer)
  10. 8 | int b
  11. | [sizeof=16, dsize=12, align=8,
  12. | nvsize=12, nvalign=8]
  13. *** Dumping AST Record Layout
  14. 0 | class C
  15. 0 | class A (primary base)
  16. 0 | (A vtable pointer)
  17. 8 | int a
  18. 16 | class B (base)
  19. 16 | (B vtable pointer)
  20. 24 | int b
  21. 28 | int c
  22. | [sizeof=32, dsize=32, align=8,
  23. | nvsize=32, nvalign=8]

查看 class A、B、C 的虚函数表的所有条目:

  1. Vtable for 'A' (3 entries).
  2. 0 | offset_to_top (0)
  3. 1 | A RTTI
  4. -- (A, 0) vtable address --
  5. 2 | void A::f1()
  6. VTable indices for 'A' (1 entries).
  7. 0 | void A::f1()
  8. Vtable for 'B' (3 entries).
  9. 0 | offset_to_top (0)
  10. 1 | B RTTI
  11. -- (B, 0) vtable address --
  12. 2 | void B::f2()
  13. VTable indices for 'B' (1 entries).
  14. 0 | void B::f2()
  15. Vtable for 'C' (7 entries).
  16. 0 | offset_to_top (0)
  17. 1 | C RTTI
  18. -- (A, 0) vtable address --
  19. -- (C, 0) vtable address --
  20. 2 | void C::f1()
  21. 3 | void C::f2()
  22. 4 | offset_to_top (-16)
  23. 5 | C RTTI
  24. -- (B, 16) vtable address --
  25. 6 | void C::f2()
  26. [this adjustment: -16 non-virtual]
  27. Thunks for 'void C::f2()' (1 entry).
  28. 0 | this adjustment: -16 non-virtual
  29. VTable indices for 'C' (2 entries).
  30. 0 | void C::f1()
  31. 1 | void C::f2()

此时可以看到,在多重继承下,虚函数表多出了许多单一继承没有的条目,接下来进行仔细讨论

2.1 为什么 C 的布局中有两个 vptr?

与单链继承不同,由于 A 和 B 完全独立,它们的虚函数没有顺序关系,即 f1 和 f2 有着相同对虚表起始位置的偏移量,所以不可以按照偏移量的顺序排布;并且 A 和 B 中的成员变量也是无关的,因此基类间也不具有包含关系;这使得 A 和 B 在 C 中必须要处于两个不相交的区域中,同时需要有两个虚指针分别对它们虚函数表索引

2.2 class C 对象的内存布局中 primary base 是何意义?

再次关注一下 class C 的对象的内存布局:

  1. *** Dumping AST Record Layout
  2. 0 | class C
  3. 0 | class A (primary base)
  4. 0 | (A vtable pointer)
  5. 8 | int a
  6. 16 | class B (base)
  7. 16 | (B vtable pointer)
  8. 24 | int b
  9. 28 | int c
  10. | [sizeof=32, dsize=32, align=8,
  11. | nvsize=32, nvalign=8]

已经知道 class C 是 public 方式继承了 class A 与 class B,而 class A 被标记为primary base,其意义是:class C 将 class A 作为主基类,也就是将 class C 的虚函数并入class A 的虚函数表之中

2.3 多重继承情况下,class C 的虚函数表 vtable 的特点?

多重继承情况下,class C 的虚函数表内容如下:

  1. Vtable for 'C' (7 entries).
  2. 0 | offset_to_top (0)
  3. 1 | C RTTI
  4. -- (A, 0) vtable address --
  5. -- (C, 0) vtable address --
  6. 2 | void C::f1()
  7. 3 | void C::f2()
  8. 4 | offset_to_top (-16)
  9. 5 | C RTTI
  10. -- (B, 16) vtable address --
  11. 6 | void C::f2()
  12. [this adjustment: -16 non-virtual]
  13. Thunks for 'void C::f2()' (1 entry).
  14. 0 | this adjustment: -16 non-virtual
  15. VTable indices for 'C' (2 entries).
  16. 0 | void C::f1()
  17. 1 | void C::f2()

可以看到,class C 的整个虚函数表其实是两个虚函数表拼接而成(这也就对应了 class C 为什么由两个 vptr)

一步步分析,先看上半部分的虚函数表:

  1. 0 | offset_to_top (0)
  2. 1 | C RTTI
  3. -- (A, 0) vtable address --
  4. -- (C, 0) vtable address --
  5. 2 | void C::f1()
  6. 3 | void C::f2()

前面已经提到过,class C 会把 class A 当作主基类,并把自己的虚函数并入到 class A 的虚函数表之中,所以,可以才会看到如上的内容

所以,class C 中的一根 vptr 会指向这个虚函数表

再看下半部分的虚函数表:

  1. 4 | offset_to_top (-16)
  2. 5 | C RTTI
  3. -- (B, 16) vtable address --
  4. 6 | void C::f2()
  5. [this adjustment: -16 non-virtual]
  6. Thunks for 'void C::f2()' (1 entry).
  7. 0 | this adjustment: -16 non-virtual

注意,此时的 offset_to_top 中的偏移量已经是 16 了

之前说过,offset_to_top 的意义是:将对象从当前这个类型转换为该对象的实际类型的地址偏移量

在多继承中,以 class A、B、C 为例,class A 和 class B 以及 class C 类型的指针或者引用都可以指向 class C 类型的实例,比如:

  1. C cc = new C();
  2. B &bb = cc;
  3. bb.f1(); // 我们知道,由于多态,此时实际调用的class C中的虚函数f1(),即相当于cc.f1()
  4. // 回顾class C的对象的内存布局
  5. // 当我们用 B类型的引用接收cc对象时,this指针相当于指在了`16 | class B (base)`这个地方,要想实现多态,需要将this指针向上偏移16个字节,这样this指针才能指向cc对象的起始地址,编译器才能以C类型来解释cc这个对象而不会出错
  6. *** Dumping AST Record Layout
  7. 0 | class C
  8. 0 | class A (primary base)
  9. 0 | (A vtable pointer)
  10. 8 | int a
  11. 16 | class B (base)
  12. 16 | (B vtable pointer)
  13. 24 | int b
  14. 28 | int c
  15. | [sizeof=32, dsize=32, align=8,
  16. | nvsize=32, nvalign=8]

在多继承中,由于不同的基类起点可能处于不同的位置,因此当需要将它们转化为实际类型时,this 指针的偏移量也不相同,且由于多态的特性,cc 的实际类型在编译时期是无法确定的;那必然需要一个东西帮助我们在运行时期确定 cc 的实际类型,这个东西就是offset_to_top。通过让this指针加上offset_to_top的偏移量,就可以让 this 指针指向实际类型的起始地址

class C 下半部分的虚函数表还有一个值得注意的地方:

  1. 6 | void C::f2()
  2. [this adjustment: -16 non-virtual]
  3. Thunks for 'void C::f2()' (1 entry).
  4. 0 | this adjustment: -16 non-virtual

意思是,当以 B 类型的指针或者引用接受了 class C 的对象并调用 f2 时:需要将 this 指针调整-16 个字节,然后再进行调用(这跟上面所说的一样,将 this 向上调整 16 个字节就是让 this 指向 class C 对象的起始地址,从而编译器会以 class C 这个类型来看待 this 指针),然后再调用 f2,也就确保了调用的是 class C 的虚函数表中自己的 f2

3 虚拟继承

首先考虑如下代码中的 class A、B、C、D:

  1. class A {
  2. public:
  3. int a;
  4. virtual void fa() {}
  5. };
  6. class B : public virtual A {
  7. public:
  8. int b;
  9. virtual void fb() {}
  10. };
  11. class C : public virtual A {
  12. public:
  13. int c;
  14. virtual void fc() {}
  15. };
  16. class D : public B, public C {
  17. public:
  18. int c;
  19. void fa() override {}
  20. virtual void fd() {}
  21. };
  22. int main() {
  23. A *a1 = new A();
  24. B *b1 = new B();
  25. C *c1 = new C();
  26. D *d1 = new D();
  27. return 0;
  28. }

class B、C 都是以虚拟继承的方式继承 class A

对于编译器来说,要支持虚拟继承实在要花费很大的一番功夫,因为编译器不仅需要在 class D 中只保存一份 class A 的成员变量,还要确保多态行为的正确性

还是先打印出相应的对象布局以及 vtable 布局:

  1. *** Dumping AST Record Layout
  2. 0 | class A
  3. 0 | (A vtable pointer)
  4. 8 | int a
  5. | [sizeof=16, dsize=12, align=8,
  6. | nvsize=12, nvalign=8]
  7. *** Dumping AST Record Layout
  8. 0 | class B
  9. 0 | (B vtable pointer)
  10. 8 | int b
  11. 16 | class A (virtual base)
  12. 16 | (A vtable pointer)
  13. 24 | int a
  14. | [sizeof=32, dsize=28, align=8,
  15. | nvsize=12, nvalign=8]
  16. *** Dumping AST Record Layout
  17. 0 | class C
  18. 0 | (C vtable pointer)
  19. 8 | int c
  20. 16 | class A (virtual base)
  21. 16 | (A vtable pointer)
  22. 24 | int a
  23. | [sizeof=32, dsize=28, align=8,
  24. | nvsize=12, nvalign=8]
  25. *** Dumping AST Record Layout
  26. 0 | class D
  27. 0 | class B (primary base)
  28. 0 | (B vtable pointer)
  29. 8 | int b
  30. 16 | class C (base)
  31. 16 | (C vtable pointer)
  32. 24 | int c
  33. 28 | int c
  34. 32 | class A (virtual base)
  35. 32 | (A vtable pointer)
  36. 40 | int a
  37. | [sizeof=48, dsize=44, align=8,
  38. | nvsize=32, nvalign=8]
  39. Vtable for 'A' (5 entries).
  40. 0 | offset_to_top (0)
  41. 1 | A RTTI
  42. -- (A, 0) vtable address --
  43. 2 | void A::f1()
  44. 3 | void A::f2()
  45. 4 | void A::f3()
  46. VTable indices for 'A' (3 entries).
  47. 0 | void A::f1()
  48. 1 | void A::f2()
  49. 2 | void A::f3()
  50. Vtable for 'B' (14 entries).
  51. 0 | vbase_offset (16)
  52. 1 | offset_to_top (0)
  53. 2 | B RTTI
  54. -- (B, 0) vtable address --
  55. 3 | void B::f1()
  56. 4 | void B::f2()
  57. 5 | void B::fb()
  58. 6 | vcall_offset (0)
  59. 7 | vcall_offset (-16)
  60. 8 | vcall_offset (-16)
  61. 9 | offset_to_top (-16)
  62. 10 | B RTTI
  63. -- (A, 16) vtable address --
  64. 11 | void B::f1()
  65. [this adjustment: 0 non-virtual, -24 vcall offset offset]
  66. 12 | void B::f2()
  67. [this adjustment: 0 non-virtual, -32 vcall offset offset]
  68. 13 | void A::f3()
  69. Virtual base offset offsets for 'B' (1 entry).
  70. A | -24
  71. Thunks for 'void B::f1()' (1 entry).
  72. 0 | this adjustment: 0 non-virtual, -24 vcall offset offset
  73. Thunks for 'void B::f2()' (1 entry).
  74. 0 | this adjustment: 0 non-virtual, -32 vcall offset offset
  75. VTable indices for 'B' (3 entries).
  76. 0 | void B::f1()
  77. 1 | void B::f2()
  78. 2 | void B::fb()
  79. Vtable for 'C' (14 entries).
  80. 0 | vbase_offset (16)
  81. 1 | offset_to_top (0)
  82. 2 | C RTTI
  83. -- (C, 0) vtable address --
  84. 3 | void C::f1()
  85. 4 | void C::f2()
  86. 5 | void C::fc()
  87. 6 | vcall_offset (0)
  88. 7 | vcall_offset (-16)
  89. 8 | vcall_offset (-16)
  90. 9 | offset_to_top (-16)
  91. 10 | C RTTI
  92. -- (A, 16) vtable address --
  93. 11 | void C::f1()
  94. [this adjustment: 0 non-virtual, -24 vcall offset offset]
  95. 12 | void C::f2()
  96. [this adjustment: 0 non-virtual, -32 vcall offset offset]
  97. 13 | void A::f3()
  98. Virtual base offset offsets for 'C' (1 entry).
  99. A | -24
  100. Thunks for 'void C::f1()' (1 entry).
  101. 0 | this adjustment: 0 non-virtual, -24 vcall offset offset
  102. Thunks for 'void C::f2()' (1 entry).
  103. 0 | this adjustment: 0 non-virtual, -32 vcall offset offset
  104. VTable indices for 'C' (3 entries).
  105. 0 | void C::f1()
  106. 1 | void C::f2()
  107. 2 | void C::fc()
  108. Vtable for 'D' (21 entries).
  109. 0 | vbase_offset (32)
  110. 1 | offset_to_top (0)
  111. 2 | D RTTI
  112. -- (B, 0) vtable address --
  113. -- (D, 0) vtable address --
  114. 3 | void D::f1()
  115. 4 | void D::f2()
  116. 5 | void B::fb()
  117. 6 | void D::fd()
  118. 7 | vbase_offset (16)
  119. 8 | offset_to_top (-16)
  120. 9 | D RTTI
  121. -- (C, 16) vtable address --
  122. 10 | void D::f1()
  123. [this adjustment: -16 non-virtual]
  124. 11 | void D::f2()
  125. [this adjustment: -16 non-virtual]
  126. 12 | void C::fc()
  127. 13 | vcall_offset (0)
  128. 14 | vcall_offset (-32)
  129. 15 | vcall_offset (-32)
  130. 16 | offset_to_top (-32)
  131. 17 | D RTTI
  132. -- (A, 32) vtable address --
  133. 18 | void D::f1()
  134. [this adjustment: 0 non-virtual, -24 vcall offset offset]
  135. 19 | void D::f2()
  136. [this adjustment: 0 non-virtual, -32 vcall offset offset]
  137. 20 | void A::f3()
  138. Virtual base offset offsets for 'D' (1 entry).
  139. A | -24
  140. Thunks for 'void D::f1()' (2 entries).
  141. 0 | this adjustment: -16 non-virtual
  142. 1 | this adjustment: 0 non-virtual, -24 vcall offset offset
  143. Thunks for 'void D::f2()' (2 entries).
  144. 0 | this adjustment: -16 non-virtual
  145. 1 | this adjustment: 0 non-virtual, -32 vcall offset offset
  146. VTable indices for 'D' (3 entries).
  147. 0 | void D::f1()
  148. 1 | void D::f2()
  149. 3 | void D::fd()
  150. Construction vtable for ('B', 0) in 'D' (14 entries).
  151. 0 | vbase_offset (32)
  152. 1 | offset_to_top (0)
  153. 2 | B RTTI
  154. -- (B, 0) vtable address --
  155. 3 | void B::f1()
  156. 4 | void B::f2()
  157. 5 | void B::fb()
  158. 6 | vcall_offset (0)
  159. 7 | vcall_offset (-32)
  160. 8 | vcall_offset (-32)
  161. 9 | offset_to_top (-32)
  162. 10 | B RTTI
  163. -- (A, 32) vtable address --
  164. 11 | void B::f1()
  165. [this adjustment: 0 non-virtual, -24 vcall offset offset]
  166. 12 | void B::f2()
  167. [this adjustment: 0 non-virtual, -32 vcall offset offset]
  168. 13 | void A::f3()
  169. Construction vtable for ('C', 16) in 'D' (14 entries).
  170. 0 | vbase_offset (16)
  171. 1 | offset_to_top (0)
  172. 2 | C RTTI
  173. -- (C, 16) vtable address --
  174. 3 | void C::f1()
  175. 4 | void C::f2()
  176. 5 | void C::fc()
  177. 6 | vcall_offset (0)
  178. 7 | vcall_offset (-16)
  179. 8 | vcall_offset (-16)
  180. 9 | offset_to_top (-16)
  181. 10 | C RTTI
  182. -- (A, 32) vtable address --
  183. 11 | void C::f1()
  184. [this adjustment: 0 non-virtual, -24 vcall offset offset]
  185. 12 | void C::f2()
  186. [this adjustment: 0 non-virtual, -32 vcall offset offset]
  187. 13 | void A::f3()

先分析 classB、C;由于 class B、C 基本相同,所以此处只分析 class B,先单独看 class B 的对象的内存布局:

  1. *** Dumping AST Record Layout
  2. 0 | class B
  3. 0 | (B vtable pointer)
  4. 8 | int b
  5. 16 | class A (virtual base)
  6. 16 | (A vtable pointer)
  7. 24 | int a
  8. | [sizeof=32, dsize=28, align=8,
  9. | nvsize=12, nvalign=8]

对比可以看出,在 class B 的对象的内存布局上,虚拟继承与普通继承的最大区别在于:虚拟继承下,class B 的内存布局不再是 class A 的内容在最前面然后紧接着 class B 的内容,而是先是 class B 的内容,然后再接着 class A 的内容

这种布局看起来就像在 class B 对象的后面接上一个 class A 对象,观察一下左边显示的偏移量:

可以看到 class A 的 vptr 的偏移量为 16,在 class A 之前,就是 class B 的内容了,class B 只含有一根 vptr(8 字节)+一个 int(4 字节)=12 字节,然而 class A 的 vptr 的偏移量却是 16,也就是说,class B 的对象完成了边界调整(12 调整到 16),然后再在后面拼接上 class A 的对象

再分析一下 class B 的 vtable:

  1. Vtable for 'B' (14 entries).
  2. 0 | vbase_offset (16)
  3. 1 | offset_to_top (0)
  4. 2 | B RTTI
  5. -- (B, 0) vtable address --
  6. 3 | void B::f1()
  7. 4 | void B::f2()
  8. 5 | void B::fb()
  9. 6 | vcall_offset (0)
  10. 7 | vcall_offset (-16)
  11. 8 | vcall_offset (-16)
  12. 9 | offset_to_top (-16)
  13. 10 | B RTTI
  14. -- (A, 16) vtable address --
  15. 11 | void B::f1()
  16. [this adjustment: 0 non-virtual, -24 vcall offset offset]
  17. 12 | void B::f2()
  18. [this adjustment: 0 non-virtual, -32 vcall offset offset]
  19. 13 | void A::f3()
  20. Virtual base offset offsets for 'B' (1 entry).
  21. A | -24
  22. Thunks for 'void B::f1()' (1 entry).
  23. 0 | this adjustment: 0 non-virtual, -24 vcall offset offset
  24. Thunks for 'void B::f2()' (1 entry).
  25. 0 | this adjustment: 0 non-virtual, -32 vcall offset offset
  26. VTable indices for 'B' (3 entries).
  27. 0 | void B::f1()
  28. 1 | void B::f2()
  29. 2 | void B::fb()

vbase_offset (16):用来访问虚基类子对象的偏移量(结合 class B 的对象内存布局观察)

vcall_offset(-16):当 class A 的引用 a 实际接受的是 class B 对象,然后执行 a→f1()(或 f2),由于 f1(或 f2)在 class B 中被重写过了,而此时的 this 表示的是一个 class A 类型的对象,所以需要对 this 进行调整才能正确的调用到 B::f1()(或 f2),this 如何调整?靠的就是这个 vcall_offset(-16)即将 this 指针向上调整 16 个字节,然后再调用 f1()(或 f2)

vcall_offset(0):当 class A 的引用 a 实际接受的是 class B 对象,然后执行 a→f3(),由于 f3 并没有被 class B 重写,所以此时的 this 不需要进行调整,所以 vcall_offset 为 0

对于 class D 的 vtable 来说,只是变得更加复杂而已,其中的条目在之前已经全部介绍过了,可以自行进行分析

PS:个人分析,不对的地方请指正

Vtable内存布局分析的更多相关文章

  1. JVM 系列(4)一看就懂的对象内存布局

    请点赞关注,你的支持对我意义重大. Hi,我是小彭.本文已收录到 GitHub · AndroidFamily 中.这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] ...

  2. HotSpot源码分析之C++对象的内存布局

    HotSpot采用了OOP-Klass模型来描述Java类和对象.OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象的具体类型.为了更好理解这个模型, ...

  3. 从C++对象内存布局和构造过程来具体分析C++中的封装、继承、多态

    一.封装模型的内存布局 常见类对象的成员可能包含以下元素: 内建类型.指针.引用.组合对象.虚函数. 另一个角度的分类: 数据成员:静态.非静态 成员函数:静态.非静态.虚函数 1.仅包含内建类型的场 ...

  4. 从汇编看c++中的虚拟继承及内存布局(二)

    下面是c++源码: class Top {//虚基类 public: int i; Top(int ii) { i = ii; } virtual int getTop() { cout <&l ...

  5. C++对象的内存布局以及虚函数表和虚基表

    C++对象的内存布局以及虚函数表和虚基表 本文为整理文章, 参考: http://blog.csdn.net/haoel/article/details/3081328 http://blog.csd ...

  6. 图说C++对象模型:对象内存布局详解

    0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看. 本文的结论都在VS2013上得到验证.不同的编译器在内存布局的细节上可能有 ...

  7. C++ 系列:内存布局

    转载自http://www.cnblogs.com/skynet/archive/2011/03/07/1975479.html 为什么需要知道C/C++的内存布局和在哪可以可以找到想要的数据?知道内 ...

  8. c/c++ 对象内存布局

    一.对象内存查看工具 VS 编译器 CL 的一个编译选项可以查看 C++ 类的内存布局,非常有用.使用如下,从开始程序菜单找到 Visual Stdio 2012. 选择 VS 的命令行工具,按如下格 ...

  9. C++ 多继承和虚继承的内存布局(转)

    转自:http://www.oschina.net/translate/cpp-virtual-inheritance 警告. 本文有点技术难度,需要读者了解C++和一些汇编语言知识. 在本文中,我们 ...

随机推荐

  1. 实验吧CTF练习题---WEB---Forms解析

    实验吧web之Forms 地址:http://www.shiyanbar.com/ctf/1819 flag值:ctf{forms_are_easy}   解题步骤: 1.查看页面源代码,从中发现&q ...

  2. 泛型接口、JAVA API、包装类

    泛型接口就是拥有一个或多个类型参数的接口 语法: public interface 接口名<类型形参>{ 方法名(类型形参 类型形参实例); } 示例: public interface ...

  3. sql server之SQL SELECT INTO 语句

    SELECT INTO 语句可用于创建表的备份复件. SELECT INTO 语句 SELECT INTO 语句从一个表中选取数据,然后把数据插入另一个表中. SELECT INTO 语句常用于创建表 ...

  4. 第1次作业:使用Packet Tracer分析HTTP数据包

    个人信息:      •  姓名:李微微       •  班级:计算1811       •  学号:201821121001 一.摘要 本文将会描述使用Packet Tracer工具用到的网络结构 ...

  5. Java程序连接数据库

    /** * 了解: 利用 Driver 接口的 connect 方法获取连接 */ // 第一种实现 /** * 了解: 利用 Driver 接口的 connect 方法获取连接 */ @Test p ...

  6. NPOI 导出添加批注功能

    这个问题在网上搜,都是说如下即可: //添加批注HSSFPatriarch patr = (HSSFPatriarch)sheet.CreateDrawingPatriarch();HSSFComme ...

  7. 微信支付JSAPI支付

    1.介绍 JSAPI支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付.应用场景有:    ◆ 用户在微信公众账号内进入商家公众号,打开某 ...

  8. netCDF4 not installed properly - DLL load failed (netCDF4安装问题)

    环境描述:windows10 ,conda,python3.6 问题描述:netCDF4是python中用来处理地球气象数据的文件读取包,在安装完成后,from netCDF4 import Data ...

  9. [转载 ]五种常见的 PHP 设计模式

    五种常见的 PHP 设计模式 策略模式 策略模式是对象的行为模式,用意是对一组算法的封装.动态的选择需要的算法并使用. 策略模式指的是程序中涉及决策控制的一种模式.策略模式功能非常强大,因为这个设计模 ...

  10. JS中3种风格的For循环有什么异同?

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者.原文出处:https://blog.bitsrc.io/3-flavors-of-the-for-loop-i ...