再议内存布局,你学会了吗?

由于不同的基类起点可能处于不同的位置,因此当需要将它们转化为实际类型时,this指针的偏移量也不相同
首页 新闻资讯 行业资讯 再议内存布局,你学会了吗?

你好,我是雨乐!

在上一篇文章C++:从技术角度聊聊RTTI中聊到了虚函数表,以及内部的部分布局。对于c++对象的内存布局一直处于似懂非懂似清非清的阶段,没有去深入了解过,所以借着这个机会,一并分析下。

多态在我们日常工作中用的算是比较多的一种特性,业界编译器往往是通过虚函数来实现运行时多态,而涉及到虚函数的内存布局往往是最麻烦且容易出错的,本文从一个简单的例子入手,借助gcc和gdb,对内存布局进行分析,相信看完本文,对内存布局会有一个清晰的认识。

多态

众所周知,C++为了实现多态(运行期),引进了虚函数(语言标准支持的,其它实现方式不在本文讨论范围内),而虚函数的实现机制则是通过虚函数表。这块的知识点不算多,却非常重要,因此往往是面试必问之一,当然,对于我也不例外。作为候选人,如果没有把运行期多态的实现机制讲清楚,那么此次面试基本凉凉~~

仍然以上一篇文章的代码为例,代码如下:

复制

class Base1 {public:
  virtual void fun() {}
  virtual void f1() {}
  int a;};class Derived : public Base {public:
  void fun() {}  // override Base::fun()
  int b;};void call(Base *b) {
  b->fun();}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

在上述示例call()函数中,当b指向Base对象时候,call()函数实际调用的是Base::fun();当b指向Derived对象时候,call()函数实际调用的是Derived::fun()。之所以可以这么实现,是因为虚函数后面的实现机制--虚函数表(后面称为Vtable):

• 对于每个类(存在虚函数,后面文中不再赘述),存在一个表,表的内容包含虚函数等(不仅仅是虚函数,在后面会有细讲),类似于如下这种:

复制

vtable_Base = {&Base::func, ...}vtable_Derived = {&Derived::func, ...}
  • 1.

  • 2.

• 在创建类对象时候,对象最前部会有一个指针(称之为vptr),指向给类虚函数表的对应位置。PS:(需要注意的是并不是指向Vtable的头,这块一定要注意)

那么,call()函数在运行的时候,因为不知道其参数b所指向具体类型是什么,所以只能通过其它方式进行调用。在前面的内容中,有提到过每个对象会有一个指针指向其类的虚函数表,那么就可以通过该虚函数表进行相应的调用。因此,call()函数中的b->fun()就类似于如下:

复制

((Vtable*)b)[0]()
  • 1.

在现在编译器对多态的实现中,原理与上述差不多,只是更为复杂。比如在在虚函数指针的索引(如上述例子中的index 0),这个index是根据函数的声明顺序而来,如果在Derived中再新增一个virtual函数fun2(),那么其在虚函数表中的index就是1。

实现

本节中以一个多继承作为示例,代码如下:

复制

class Base1 {public:
  void f0() {}
  virtual void f1() {}
  int a;};class Base2 {public:
  virtual void f2() {}
  int b;};class Derived : public Base1, public Base2 {public:
  void d() {}
  void f2() {}  // override Base2::f1()
  int c;};int main() {
  Base2 *b2 = new Base2;
  Derived *d = new Derived;}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

后面的内容将分别从基类和派生类的角度进行分析。

基类

首先,我们通过g++的命令-fdump-class-hierarchy进行编译,以便在布局上有一个宏观的认识,然后通过gdb进行更加详细的分析。

Base2内存布局如下:

复制

Vtable for Base2
Base2::_ZTV5Base2: 3u entries0     (int (*)(...))08     (int (*)(...))(& _ZTI5Base2)16    (int (*)(...))Base2::f2

Class Base2
   size=16 align=8
   base size=12 base align=8Base2 (0x0x7ff572e6b600) 0vptr=((& Base2::_ZTV5Base2) + 16u)
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

在上述代码中,Base2的Vtable名为 _ZTV5Base2 ,经过c++filt处理之后,发现其为vtable for Base2。之所以是这种是因为被编译器进行了mangled。其中,TV代表Table for Virtual,后面的数字5是类名的字符数,Base2则是类名。

维基百科以g++3.4.6为示例,示例中之处Vtable应该只包含指向Base2::f2 的指针,但在我的本地环境(g++5.4.0,布局如上述)中,B2::f2为第三行:首先是offset,其值为0;然后包含一个指向名为_ZTI5Base2的结构的指针(这个在上节RTTI一文中有讲,在本文后面也会涉及);最后是函数指针B2::f2。

g++ 3.4.6 from GCC produces the following 32-bit memory layout for the object b2:[nb 1]

b2:  +0: pointer to virtual method table of Base2  +4: value of bvirtual method table of B2:  +0: Base2::f2()   

继续看Class Base2部分,我们注意到有一句vptr=((& Base2::_ZTV5Base2) + 16u),通过这句可以知道,Base2类中其虚函数指针vptr指向其虚函数表的首位+16处。

在下面的内容中,将通过gdb来分析其内存布局。

复制

(gdb) disas
Dump of assembler code for function main:
   0x00000000004006f8 <+0>: push   %rbp   0x00000000004006f9 <+1>: mov    %rsp,%rbp   0x00000000004006fc <+4>: push   %rbx   0x00000000004006fd <+5>: sub    $0x18,%rsp=> 0x0000000000400701 <+9>: mov    $0x10,%edi   0x0000000000400706 <+14>: callq  0x400578 <_Znwm@plt>
   0x000000000040070b <+19>: mov    %rax,%rbx   0x000000000040070e <+22>: mov    %rbx,%rdi   0x0000000000400711 <+25>: callq  0x40076a <_ZN5Base2C2Ev>
   0x0000000000400716 <+30>: mov    %rbx,-0x18(%rbp)
   0x000000000040071a <+34>: mov    $0x20,%edi   0x000000000040071f <+39>: callq  0x400578 <_Znwm@plt>
   0x0000000000400724 <+44>: mov    %rax,%rbx   0x0000000000400727 <+47>: mov    %rbx,%rdi   0x000000000040072a <+50>: callq  0x40079a <_ZN7DerivedC2Ev>
   0x000000000040072f <+55>: mov    %rbx,-0x20(%rbp)
   0x0000000000400733 <+59>: mov    $0x0,%eax   0x0000000000400738 <+64>: add    $0x18,%rsp   0x000000000040073c <+68>: pop    %rbx   0x000000000040073d <+69>: pop    %rbp   0x000000000040073e <+70>: retq
End of assembler dump.(gdb) b *0x0000000000400716Breakpoint 2 at 0x400716: file abc.cc, line 22.(gdb) c
Continuing.

Breakpoint 2, 0x0000000000400716 in main () at abc.cc:2222   Base2 *b2 = new Base2;(gdb) disas
Dump of assembler code for function main:
   0x00000000004006f8 <+0>: push   %rbp   0x00000000004006f9 <+1>: mov    %rsp,%rbp   0x00000000004006fc <+4>: push   %rbx   0x00000000004006fd <+5>: sub    $0x18,%rsp   0x0000000000400701 <+9>: mov    $0x10,%edi   0x0000000000400706 <+14>: callq  0x400578 <_Znwm@plt>
   0x000000000040070b <+19>: mov    %rax,%rbx   0x000000000040070e <+22>: mov    %rbx,%rdi   0x0000000000400711 <+25>: callq  0x40076a <_ZN5Base2C2Ev>=> 0x0000000000400716 <+30>: mov    %rbx,-0x18(%rbp)
   0x000000000040071a <+34>: mov    $0x20,%edi   0x000000000040071f <+39>: callq  0x400578 <_Znwm@plt>
   0x0000000000400724 <+44>: mov    %rax,%rbx   0x0000000000400727 <+47>: mov    %rbx,%rdi   0x000000000040072a <+50>: callq  0x40079a <_ZN7DerivedC2Ev>
   0x000000000040072f <+55>: mov    %rbx,-0x20(%rbp)
   0x0000000000400733 <+59>: mov    $0x0,%eax   0x0000000000400738 <+64>: add    $0x18,%rsp   0x000000000040073c <+68>: pop    %rbx   0x000000000040073d <+69>: pop    %rbp   0x000000000040073e <+70>: retq
End of assembler dump.
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

  • 41.

  • 42.

  • 43.

  • 44.

  • 45.

  • 46.

  • 47.

  • 48.

  • 49.

  • 50.

  • 51.

  • 52.

  • 53.

  • 54.

  • 55.

在上述汇编中<+14>处,调用了operator new进行内存分配,然后将地址放于寄存器rax中,在<+25>处调用Base2构造函数,继续分析:

复制

(gdb) p/x $rax
$2 = 0x612c20(gdb) x/2xg 0x612c200x612c20: 0x0000000000400918 0x0000000000000000(gdb) p &(((Base2*)0)->b)$3 = (int *) 0x8
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

首先通过p/x $rax获取b2的地址0x612c20,然后通过x/4xg 0x612c20打印内存地址,地址信息包含存储的属性;接着通过p &(((Base2*)0)->b)来获取变量b的布局,其值为0x8,因此可以说明变量b在类Base2的第八字节处,即vptr之后,那么class base2的结构布局如下:

图片

在上述x/2xg 0x612c20的输出中,有个地址0x0000000000400918,其指向Base2类的虚函数表,这个可以通过如下方式进行验证:

复制

(gdb) p *((Base2*)0x612c20)$6 = {_vptr.Base2 = 0x400918, b = 0}
  • 1.

  • 2.

但是需要注意的是,其并不是指向虚函数表的首位,而是指向Vtable + 0x10处,下面是类Base2虚函数表的内容:

复制

(gdb) x/4xg 0x0000000000400918-0x100x400908 <_ZTV5Base2>: 0x0000000000000000 0x00000000004009800x400918 <_ZTV5Base2+16>: 0x000000000040074c 0x0000000000000000(gdb) x/2i 0x000000000040074c0x40074c <_ZN5Base22f2Ev>: push   %rbp0x40074d <_ZN5Base22f2Ev+1>: mov    %rsp,%rbp
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

其中,0代表offset,第三项0x400918值与_vptr.Base2一致,其中的内容通过x/2i 0x000000000040074c分析可以看出为Base2::f2()函数地址。那么第二项又代表什么呢?

还记得上篇文章中的RTTI信息么?对!第二项就是指向RTTI信息的地址,可以通过如下命令:

复制

(gdb) x/2xg 0x00000000004009800x400980 <_ZTI5Base2>: 0x0000000000600da0 0x0000000000400990(gdb) x/s 0x00000000004009900x400990 <_ZTS5Base2>:  "5Base2"
  • 1.

  • 2.

  • 3.

  • 4.

其中,_ZTI5Base2代表typeinfo for Base2,其指向的地址有两个内容,分别是0x0000000000600da0和0x0000000000400990,其中0x400990存储的是类名,可以通过x/s来证明。

然后接着分析0x0000000000600da0存储的内容,如下:

复制

(gdb) x/2xg 0x0000000000600da00x600da0 <_ZTVN10__cxxabiv117__class_type_infoE@@CXXABI_1.3+16>: 0x0000003e9628b210 0x0000003e9628b230
  • 1.

  • 2.

_ZTVN10__cxxabiv117__class_type_infoE解析之后为vtable for __cxxabiv1::__class_type_info。

综上,类Base2的内存布局如下图所示:

图片

多重继承

跟上节一样,仍然通过 -fdump-class-hierarchy 参数获取Derived类的详细信息,如下:

复制

Vtable for Derived
Derived::_ZTV7Derived: 7u entries0     (int (*)(...))08     (int (*)(...))(& _ZTI7Derived)16    (int (*)(...))Base1::f124    (int (*)(...))Derived::f232    (int (*)(...))-1640    (int (*)(...))(& _ZTI7Derived)48    (int (*)(...))Derived::_ZThn16_N7Derived2f2Ev

Class Derived
   size=32 align=8
   base size=32 base align=8Derived (0x0x7f2708268af0) 0vptr=((& Derived::_ZTV7Derived) + 16u)
  Base1 (0x0x7f2708127840) 0  primary-for Derived (0x0x7f2708268af0)
  Base2 (0x0x7f27081278a0) 16  vptr=((& Derived::_ZTV7Derived) + 48u)
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

接着继续使用gdb进行分析:

复制

(gdb) disas
Dump of assembler code for function main:
   0x00000000004006f8 <+0>: push   %rbp   0x00000000004006f9 <+1>: mov    %rsp,%rbp   0x00000000004006fc <+4>: push   %rbx   0x00000000004006fd <+5>: sub    $0x18,%rsp   0x0000000000400701 <+9>: mov    $0x10,%edi   0x0000000000400706 <+14>: callq  0x400578 <_Znwm@plt>
   0x000000000040070b <+19>: mov    %rax,%rbx   0x000000000040070e <+22>: mov    %rbx,%rdi   0x0000000000400711 <+25>: callq  0x40076a <_ZN5Base2C2Ev>
   0x0000000000400716 <+30>: mov    %rbx,-0x18(%rbp)
   0x000000000040071a <+34>: mov    $0x20,%edi   0x000000000040071f <+39>: callq  0x400578 <_Znwm@plt>
   0x0000000000400724 <+44>: mov    %rax,%rbx   0x0000000000400727 <+47>: mov    %rbx,%rdi   0x000000000040072a <+50>: callq  0x40079a <_ZN7DerivedC2Ev>=> 0x000000000040072f <+55>: mov    %rbx,-0x20(%rbp)
   0x0000000000400733 <+59>: mov    $0x0,%eax   0x0000000000400738 <+64>: add    $0x18,%rsp   0x000000000040073c <+68>: pop    %rbx   0x000000000040073d <+69>: pop    %rbp   0x000000000040073e <+70>: retq
End of assembler dump.(gdb) p/x $rax
$8 = 0x612c40(gdb) p sizeof(Derived)$9 = 32(gdb)  x/6xg 0x612c400x612c40: 0x00000000004008e0 0x00000000000000000x612c50: 0x0000000000400900 0x00000000000000000x612c60: 0x0000000000000000 0x00000000000203a1(gdb) p &(((Derived*)0)->a)$15 = (int *) 0x8(gdb) p &(((Derived*)0)->b)$16 = (int *) 0x18(gdb) p &(((Derived*)0)->c)$17 = (int *) 0x1cp *((Derived*)0x612c40)$13 = {<Base1> = {_vptr.Base1 = 0x4008e0, a = 0}, <Base2> = {_vptr.Base2 = 0x400900, b = 0}, c = 0}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

从上述代码可以看出,Derived的结构布局如下:

图片

接着,我们分析类Derived的虚函数表:

复制

(gdb) x/7xg 0x00000000004008e0 - 0x100x4008d0 <_ZTV7Derived>: 0x0000000000000000 0x00000000004009380x4008e0 <_ZTV7Derived+16>: 0x0000000000400740 0x00000000004007580x4008f0 <_ZTV7Derived+32>: 0xfffffffffffffff0 0x00000000004009380x400900 <_ZTV7Derived+48>: 0x0000000000400763
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

其对应如下:

复制

Vtable for Derived
Derived::_ZTV7Derived: 7u entries0     (int (*)(...))08     (int (*)(...))(& _ZTI7Derived)16    (int (*)(...))Base1::f124    (int (*)(...))Derived::f232    (int (*)(...))-1640    (int (*)(...))(& _ZTI7Derived)48    (int (*)(...))Derived::_ZThn16_N7Derived2f2Ev
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

为了验证如上,继续使用gdb进行操作:

复制

(gdb) x/2xg 0x00000000004009380x400938 <_ZTI7Derived>: 0x0000000000600df8 0x0000000000400970(gdb) x/4xi 0x0000000000400740
   0x400740 <_ZN5Base12f1Ev>: push   %rbp   0x400741 <_ZN5Base12f1Ev+1>: mov    %rsp,%rbp   0x400744 <_ZN5Base12f1Ev+4>: mov    %rdi,-0x8(%rbp)
   0x400748 <_ZN5Base12f1Ev+8>: nop(gdb) x/2xi 0x0000000000400740
   0x400740 <_ZN5Base12f1Ev>: push   %rbp   0x400741 <_ZN5Base12f1Ev+1>: mov    %rsp,%rbp(gdb) x/2xi 0x0000000000400758
   0x400758 <_ZN7Derived2f2Ev>: push   %rbp   0x400759 <_ZN7Derived2f2Ev+1>: mov    %rsp,%rbp(gdb) x/4xi 0x0000000000400763
   0x400763 <_ZThn16_N7Derived2f2Ev>: sub    $0x10,%rdi   0x400767 <_ZThn16_N7Derived2f2Ev+4>: jmp    0x400758 <_ZN7Derived2f2Ev>
   0x400769: nop   0x40076a <_ZN5Base2C2Ev>: push   %rbp
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

在上面的内存布局中,_ZThn16_N7Derived2f2Ev在上篇文章中没有进行分析,那么这个标记代表什么意思么,其作用又是什么呢?

通过c++filt将其demanged之后,non-virtual thunk to Derived::f2()。那么这个thunk的目的或者意义在哪呢?

我们看下如下代码:

复制

Derived *d = new Derived;Base1 *b1 = (Base1*)d;Base2 *b2 = (Base2*)d;std::cout << d << " " << b1 << " " << b2 << std::endl;((Base2*)d)->f2();
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

输出如下:

复制

0x1cc0c20 0x1cc0c20 0x1cc0c30
  • 1.

可以看出,同样是一个地址,使用Base1转换的地址和使用Base2转换的地址不同,这是因为在转换的时候,对指针进行了偏移,即加上了sizeof(Base1)。

好了,言归正传。

分析下如下情况:

复制

Base1* b1 = new Derived();b1->f1();
  • 1.

  • 2.

其正常工作,不需要移动任何指针,这是因为b1指向Derived对象的首地址。

那么如下是下面这种情况呢?

复制

Base2* b2 = new Derived();// 相当于 Derived *d = new Derived;// Base2* b2 = d + sizeof(Base1);b2->f2();
  • 1.

  • 2.

  • 3.

  • 4.

对于创建对象操作,在上述代码中有大致解释,那么对于b2->f2()操作,编译器又是如何实现的呢?

其必须将b2所指向的指针调整为具体的Derived对象的其实指针,这样才能正确的调用f2。此操作可以在运行时完成,即在运行时候通过调整指针指向进行操作,但这样效率明显不高。所以为了解决效率问题,编译器引入了thunk,即在编译阶段进行生成。那么针对上面的b2->f2()操作,编译器会进行如下:

复制

void thunk_to_Derived_f2(Base2* this) {this -= sizeof(Base1);Derived::f2(this);}
  • 1.

  • 2.

  • 3.

  • 4.

我们仍然通过gdb来验证这一点,如下:

复制

(gdb) x/2i 0x0000000000400763
   0x400763 <_ZThn16_N7Derived2f2Ev>: sub    $0x10,%rdi   0x400767 <_ZThn16_N7Derived2f2Ev+4>: jmp    0x400758 <_ZN7Derived2f2Ev>
  • 1.

  • 2.

  • 3.

其中,寄存器rdi中存储的是this指针,对this指针进行-16操作,然后进行调用 Derived::f2(this) 。

继续分析虚函数表的内容,其第二项为TypeInfo信息:

复制

(gdb) x/2xg 0x00000000004009380x400938 <_ZTI7Derived>: 0x0000000000600df8 0x0000000000400970(gdb) x/2xg 0x0000000000600df80x600df8 <_ZTVN10__cxxabiv121__vmi_class_type_infoE@@CXXABI_1.3+16>: 0x0000003e9628df70 0x0000003e9628df90(gdb) x/s 0x00000000004009700x400970 <_ZTS7Derived>:  "7Derived"
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

所以,综合以上内容,class Derived的内存布局如下图所示:



通过上图,可以看出class Derived对象有两个vptr,那么有没有可能将这俩vptr合并成一个呢?

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

偏移(offset)

在前面的内容中,我们多次提到了top offset,在上节Derived的虚函数表中,有两个top offset,其值分别为0和-16,那么这个offset起什么作用呢?

在此,先给出结论:将对象从当前这个类型转换为该对象的实际类型的地址偏移量。

仍然以前面的class Derived为例,其虚函数表布局如下:

复制

Vtable for Derived
Derived::_ZTV7Derived: 7u entries0     (int (*)(...))08     (int (*)(...))(& _ZTI7Derived)16    (int (*)(...))Base1::f124    (int (*)(...))Derived::f232    (int (*)(...))-1640    (int (*)(...))(& _ZTI7Derived)48    (int (*)(...))Derived::_ZThn16_N7Derived2f2Ev
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

为了能方便理解本节内容,我们不妨将Derived虚函数表认为是 class Base1和class Base2两个类的虚函数表拼接而成 。因为是多重继承,所以编译器将先继承的那个认为是 主基类(primary base) ,因此Derived类的主基类就是class Base1。

在多继承中,当最左边的类中没有虚函数时候,编译器会将第一个有虚函数的基类移到对象的开头,这样对象的开头总是有vptr。

首先看虚函数表的前半部分,如下:

复制

0     (int (*)(...))08     (int (*)(...))(& _ZTI7Derived)16    (int (*)(...))Base1::f124    (int (*)(...))Derived::f2
  • 1.

  • 2.

  • 3.

  • 4.

正是因为编译器将class Base1作为Derived的主基类,并将自己的函数加入其中。从上述可以看出offset为0,也就是说Base1类的指针不需要偏移就可以直接访问Derived::f2()。

接着看虚函数表的下半部分:

复制

32    (int (*)(...))-1640    (int (*)(...))(& _ZTI7Derived)48    (int (*)(...))Derived::_ZThn16_N7Derived2f2Ev
  • 1.

  • 2.

  • 3.

偏移值为-16,因为是多重继承,所以class Base1和class Base2类型的指针或者引用都可以指向class Derived对象,那么又是如何调用正确的成员函数呢?

复制

Base2* b2 = new Derived;
b2->f2(); //最终调用Derived::f2();
  • 1.

  • 2.

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

结语

写这块的时候,感觉需要写的还是很多的,也有很多内容没写,比如虚拟继承、菱形继承的布局都在本文中没有体现,后面有机会再接着分析。

今天的文章就到这,我们下期见!

10    2023-01-31 08:02:18    内存布局 C++ gdb