C++ 内存布局 - Part6: 虚继承

1. 关于虚继承

虚继承可以在菱形继承体系中,防止派生类中有多份重复祖基类内容。如下图所示,如果是常规继承,Class Final中会有两份Class Base的内容。通过虚继承,即Derived1 虚继承自Base, Derived2 也虚继承自Base, 那么Final中将最终保留一份Base部分的内容。

2. 代码示例

#include <iostream>

class Base {
public:
    virtual void show() {
        std::cout << "Base::show() called" << std::endl;
    }

    int a = 1;
};

class Derived1 : virtual public Base {
public:
    void show() override {
        std::cout << "Derived1::show() called" << std::endl;
    }

    int b = 2;
};

class Derived2 : virtual public Base {
public:
    void show() override {
        std::cout << "Derived2::show() called" << std::endl;
    }

    int c = 3;
};

class Final : public Derived1, public Derived2 {
public:
    void show() override {
        std::cout << "Final::show() called" << std::endl;
    }

    int d = 4;
};

int main() {
    std::cout << "Base size: " << sizeof(Base) << std::endl;
    std::cout << "Derived1 size: " << sizeof(Derived1) << std::endl;
    std::cout << "Derived2 size: " << sizeof(Derived2) << std::endl;
    std::cout << "Final size: " << sizeof(Final) << std::endl;

    Derived1 d1;
    std::cout << "d1 addr: " << &d1 << std::endl;

    Base *bptr = &d1;
    std::cout << "bptr: " << bptr << std::endl;
    bptr->show();

    Final obj;
    std::cout << "final obj  addr: " << &obj << std::endl;

    Derived1 *d1_ptr = &obj;
    std::cout << "d1_ptr: " << d1_ptr << std::endl;
    d1_ptr->show();

    Derived2 *d2_ptr = &obj;
    std::cout << "d2_ptr: " << d2_ptr << std::endl;
    d2_ptr->show();

    Base *b_ptr = &obj;
    std::cout << "b_ptr: " << b_ptr << std::endl;
    b_ptr->show();

    return 0;
}

运行结果:

Base size: 16
Derived1 size: 32
Derived2 size: 32
Final size: 48
d1 addr: 0x7fffffffe240
bptr: 0x7fffffffe250
Derived1::show() called
final obj  addr: 0x7fffffffe210
d1_ptr: 0x7fffffffe210
Final::show() called
d2_ptr: 0x7fffffffe220
Final::show() called
b_ptr: 0x7fffffffe230
Final::show() called

3. Derived1 对象的内存布局

3.1 查看 Derived1对象的内存

(gdb) p d1
$1 = {<Base> = {_vptr.Base = 0x401158 <vtable for Derived1+56>, a = 1}, _vptr.Derived1 = 0x401138 <vtable for Derived1+24>, b = 2}

注意,虽然Base部分写在了前面,但是对于虚继承,实际内存中Base被放在了后面:

(gdb) x/8x &d1
0x7fffffffe240: 0x00401138      0x00000000      0x00000002      0x00000000
0x7fffffffe250: 0x00401158      0x00000000      0x00000001      0x00000000

可以看到,对象的开头内容是 0x00401138, 也就是_vptr.Derived1 = 0x401138 <vtable for Derived1+24>, 紧接着是Derived1的int b, 然后才是_vptr.Base = 0x401158 <vtable for Derived1+56>,然后是Base的数据成员int a.

3.2 查看虚表

1) 首先查看_vptr.Derived1 = 0x401138 <vtable for Derived1+24>

(gdb) x/x 0x401138
0x401138 <vtable for Derived1+24>:      0x00400c7c
(gdb) x/i 0x00400c7c
   0x400c7c <Derived1::show()>: push   %rbp

可见就是Derived1重写的show()

2) 继续查看 _vptr.Base = 0x401158 <vtable for Derived1+56>

(gdb) x/x 0x401158
0x401158 <vtable for Derived1+56>:      0x00400ca7
(gdb) x/i 0x00400ca7
   0x400ca7 <virtual thunk to Derived1::show()>:        mov    (%rdi),%r10
(gdb) disass 0x00400ca7,+10
Dump of assembler code from 0x400ca7 to 0x400cb1:
   0x0000000000400ca7 <virtual thunk to Derived1::show()+0>:    mov    (%rdi),%r10
   0x0000000000400caa <virtual thunk to Derived1::show()+3>:    add    -0x18(%r10),%rdi
   0x0000000000400cae <virtual thunk to Derived1::show()+7>:    jmp    0x400c7c <Derived1::show()>
   0x0000000000400cb0 <Derived2::show()+0>:     push   %rbp
End of assembler dump.

可以看到,虚表中存放的是thunk代码的地址。

这段thunk代码的用途:当通过基类指针指向Derived1对象时,即Base *bptr = &d1, 由于当前的bptr指向的是Derived1对象中的Base部分(此部分位于对象靠后的位置),而非Derived1对象的真正起始地址,因此通过bptr执行虚函数时,为了执行真正的Derived1中重写的虚函数Derived1::show(),需要调整this指针到Derived1对象的起始地址。

mov    (%rdi),%r10    # rdi中存放的是this指针,也就是指向Base part的指针,而非Derived对象的起始地址, 此操作将this的开头虚表地址条目也就是_vptr.Base = 0x401158 <vtable for Derived1+56> 放入r10

add    -0x18(%r10),%rdi  #  将_vptr.Base的虚表地址-0x18 也就是前移3个条目获取里面存放的offset, 然后将此offset 加到rdi, 执行此操作之前rdi是0x7fffffffe250, offset是0xfffffffffffffff0xf

执行add操作以后,可以得到Derived1对象的起始地址: 0x7fffffffe250 + 0xfffffffffffffff0 = 0x7fffffffe240

(gdb) x/2x 0x401158 - 0x18
0x401140 <vtable for Derived1+32>:      0xfffffff0      0xffffffff

3.3 Derived1 内存布局

 其中虚表中thunk地址前面的条目0x00401200 指向typeinfo

(gdb) x/x 0x00401200
0x401200 <typeinfo for Derived1>:       0x00601d98

4. Final 对象的内存布局

4.1 查看Final对象内存

(gdb) p obj
$22 = {<Derived1> = {<Base> = {_vptr.Base = 0x401060 <vtable for Final+88>, a = 1}, _vptr.Derived1 = 0x401020 <vtable for Final+24>,
    b = 2}, <Derived2> = {_vptr.Derived2 = 0x401040 <vtable for Final+56>, c = 3}, d = 4}

同样,这只是逻辑视图,而不是内存实际布局。

查看实际内存布局:

(gdb) x/12x &obj
0x7fffffffe210: 0x00401020      0x00000000      0x00000002      0x00000000
0x7fffffffe220: 0x00401040      0x00000000      0x00000003      0x00000004
0x7fffffffe230: 0x00401060      0x00000000      0x00000001      0x00000000

顺序依次是Derived1 part, Derived2 part, Base part

4.2 查看虚表

1)首先查看_vptr.Derived1 = 0x401020 <vtable for Final+24>

(gdb) x/32x 0x401020
0x401020 <vtable for Final+24>: 0x00400ce4      0x00000000      0x00000010      0x00000000
0x401030 <vtable for Final+40>: 0xfffffff0      0xffffffff      0x00401188      0x00000000
0x401040 <vtable for Final+56>: 0x00400d18      0x00000000      0xffffffe0      0xffffffff
0x401050 <vtable for Final+72>: 0xffffffe0      0xffffffff      0x00401188      0x00000000
0x401060 <vtable for Final+88>: 0x00400d0f      0x00000000      0x00401020      0x00000000
0x401070 <VTT for Final+8>:     0x004010b8      0x00000000      0x004010d8      0x00000000
0x401080 <VTT for Final+24>:    0x004010f8      0x00000000      0x00401118      0x00000000
0x401090 <VTT for Final+40>:    0x00401060      0x00000000      0x00401040      0x00000000
(gdb) x/i 0x00400ce4
   0x400ce4 <Final::show()>:    push   %rbp

可见虚表中的虚函数地址就是<Final::show()>

2) 继续查看_vptr.Derived2 = 0x401040 <vtable for Final+56>

(gdb) x/x 0x401040
0x401040 <vtable for Final+56>: 0x00400d18
(gdb) x/i 0x00400d18
   0x400d18 <non-virtual thunk to Final::show()>:       sub    $0x10,%rdi
(gdb) disass 0x00400d18,+10
Dump of assembler code from 0x400d18 to 0x400d22:
   0x0000000000400d18 <non-virtual thunk to Final::show()+0>:   sub    $0x10,%rdi
   0x0000000000400d1c <non-virtual thunk to Final::show()+4>:   jmp    0x400ce4 <Final::show()>

End of assembler dump.

可以看到,当使用Derived2 *d2_ptr = &obj 来调用虚函数d2_ptr->show();时,需要调整this 指针,但是这个调整比较简单,就是用当前指向Derived2 part的this指针减去0x10

3) 继续查看_vptr.Base = 0x401060 <vtable for Final+88>

(gdb) x/x 0x401060
0x401060 <vtable for Final+88>: 0x00400d0f
(gdb) x/i 0x00400d0f
   0x400d0f <virtual thunk to Final::show()>:   mov    (%rdi),%r10
(gdb) disass 0x00400d0f,+10
Dump of assembler code from 0x400d0f to 0x400d19:
   0x0000000000400d0f <virtual thunk to Final::show()+0>:       mov    (%rdi),%r10
   0x0000000000400d12 <virtual thunk to Final::show()+3>:       add    -0x18(%r10),%rdi
   0x0000000000400d16 <virtual thunk to Final::show()+7>:       jmp    0x400ce4 <Final::show()>

这个就类似于上面讨论Derived1内存布局时通过基类指针操作虚函数时的this指针调整。

4.3 Final 内存布局

其中虚表中thunk地址前面的条目0x00401188指向typeinfo :

(gdb) x/i 0x00401188
   0x401188 <typeinfo for Final>:       cwtl

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值