跳至正文

C++ 继承和虚拟继承的内存分布

  • 技术

菱形继承

菱形继承:菱形继承的问题在于数据冗余和二义性。

将子类转化成爷爷类的时候会报错,因为不知道要转化到哪个类上去,这里需要加入static_cast才行。爷爷类会在两个父类中都被定义,解决的办法是:虚拟继承。

虚拟继承让被菱形继承的父类只会存在一份,消除数据冗余,那这里就得问一下,内存是如何布局的呢?如何访问内存呢?

虚拟继承跟普通的继承不一样,普通的继承是通过将父类的内存放在子类的前面来实现,但虚拟继承父类的内存是放在后面的,那菱形继承会让父类和爷爷类的偏移不一致,那如何寻找爷爷类呢?

虚拟继承依然会前置一个值,只是不再是前置父类而是前置一个虚基表指针,指向的是内存中,父类相对于自己的偏移量。

虚继承支持基类的常规转换,主要是解决了多个派生类对基类的拷贝问题,并没有解决多重继承的二义性问题。

然后关于vs和g++的实现是不完全一样的,g++的实现是虚函数和虚基类地址偏移共享一个虚表,类的实例开始处即为所属类的虚指针。因为存在虚基类地址偏移,所以几乎每个类都会有一张独一无二的虚表。

虽然有一些大概的了解了,但是能不用就别用,这个机制很慢,内存很大,并且很违反直觉。

继承的内存分布

然后在实际实验的时候一直有一个不太理解的地方,就是虚函数表似乎总是对不上:

class A
{
public:
    virtual void Test(){return;}
    int _a;
};

class B
{
public:
    virtual int Test2(){return 1;}
    int _b;
};

class C : public A, public B
{
public:
    virtual int Test2(){return 2;}
    virtual void Test(){return;}
    int _c;
};

int main()
{
    C *c = new C();
    c->_a = 1;
    c->_b = 2;
    c->_c = 3;

    return 0;
}

然后我打开gdb去分析,去看具体的内存布局:

(gdb) start
Temporary breakpoint 1 at 0x400666: file test.cpp, line 25.
Starting program: /tmp/test
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
n
Temporary breakpoint 1, main () at test.cpp:25
25          C *c = new C();
Missing separate debuginfos, use: debuginfo-install libgcc-4.8.5-44.el7.x86_64 libstdc++-4.8.5-44.el7.x86_64
(gdb) n
26          c->_a = 1;
(gdb)
27          c->_b = 2;
(gdb)
28          c->_c = 3;
(gdb)
30          return 0;
(gdb) x /16wa c
0x602010:       0x400830 <_ZTV1C+16>    0x0     0x1     0x0
0x602020:       0x400850 <_ZTV1C+48>    0x0     0x2     0x3
(gdb) x /8wa 0x400830
0x400830 <_ZTV1C+16>:   0x400702 <C::Test()>    0x0     0x4006ec <C::Test2()>   0x0
0x400840 <_ZTV1C+32>:   0xfffffffffffffff0      0xffffffffffffffff      0x4008a0 <_ZTI1C>       0x0
(gdb) x /8wa 0x400850
0x400850 <_ZTV1C+48>:   0x4006fb <_ZThn16_N1C5Test2Ev>  0x0     0x0     0x0
0x400860 <_ZTV1B>:      0x0     0x0     0x4008e0 <_ZTI1B>       0x0

其实很好理解的,0x400830是A类的虚函数指针指向虚函数表,虚函数表第一项是A的Test,第二项是B的Test2,那如果转化成A类也能按照同样的规则获取到Test这个虚函数。

那0x400850是什么呢?它所指向的0x4006fb <_ZThn16_N1C5Test2Ev>是什么意思呢?真的琢磨了很久,然后想起来gdb 的x指令里/i是可以输出汇编指令。

再进一步查看这个地址的汇编:

(gdb) x /2wi 0x4006fb
   0x4006fb <_ZThn16_N1C5Test2Ev>:      sub    $0x10,%rdi
   0x4006ff <_ZThn16_N1C5Test2Ev+4>:    jmp    0x4006ec <C::Test2()>

忽然就理解了,0x400850地址依然是一个虚函数表,第一项仍然是虚函数,并且就是C::Test2,那为什么不是直接指向C::Test2呢?

因为C::Test2()是C这个类的函数,它的this应该要指向一个C的类,但是使用0x400850虚表的对象偏移是不对的,所以这个函数在实际执行C::Test2()之前要将this向前挪16个字节。

至此,我觉得我比较深刻的理解了C++的虚函数机制。

菱形继承的内存分布

之前没完全看懂的,现在我觉得又可以了,然后再回头看一次。

class A
{
public:
    virtual int Test(){return 1;}
    int _a;
};

class B: virtual public A
{
public:
    virtual int Test(){return 2;}
    int _b;
};

class C : virtual public A
{
public:
    virtual int Test(){return 3;}
    int _c;
};

class D : public B, public C
{
public:
    virtual int Test(){return 4;}
    int _d;
};

int main()
{
    D *d = new D();
    d->_a = 1;
    d->_b = 2;
    d->_c = 3;
    d->_d = 4;

    return 0;
}

这是一个经典的菱形继承,再来看一下内存分布就比较清晰了:

(gdb) start
Temporary breakpoint 1 at 0x400666: file test.cpp, line 31.
Starting program: /tmp/test
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64

Temporary breakpoint 1, main () at test.cpp:31
31          D *d = new D();
Missing separate debuginfos, use: debuginfo-install libgcc-4.8.5-44.el7.x86_64 libstdc++-4.8.5-44.el7.x86_64
(gdb) n
32          d->_a = 1;
(gdb)
33          d->_b = 2;
(gdb)
34          d->_c = 3;
(gdb)
35          d->_d = 4;
(gdb)
37          return 0;
(gdb) p *d
$1 = {<B> = {<A> = {_vptr.A = 0x400958 <vtable for D+88>, _a = 1}, _vptr.B = 0x400918 <vtable for D+24>, _b = 2}, <C> = {_vptr.C = 0x400938 <vtable for D+56>, _c = 3}, _d = 4}
(gdb) print sizeof(D)
$2 = 48
(gdb) x /16wa d
0x603010:       0x400918 <_ZTV1D+24>    0x0     0x2     0x0
0x603020:       0x400938 <_ZTV1D+56>    0x0     0x3     0x4
0x603030:       0x400958 <_ZTV1D+88>    0x0     0x1     0x0
(gdb) x /16wa 0x400918
0x400918 <_ZTV1D+24>:   0x400722 <D::Test()>    0x0     0x10    0x0
(gdb) x /16wa 0x400938
0x400938 <_ZTV1D+56>:   0x400731 <_ZThn16_N1D4TestEv>   0x0     0xffffffffffffffe0      0xffffffffffffffff
(gdb) x /16wi 0x400731
   0x400731 <_ZThn16_N1D4TestEv>:       sub    $0x10,%rdi
   0x400735 <_ZThn16_N1D4TestEv+4>:     jmp    0x400722 <D::Test()>
(gdb) x /16wa 0x400958
0x400958 <_ZTV1D+88>:   0x400737 <_ZTv0_n24_N1D4TestEv> 0x0     0x400918 <_ZTV1D+24>    0x0
(gdb) x /16wi 0x400737
   0x400737 <_ZTv0_n24_N1D4TestEv>:     mov    (%rdi),%r10
   0x40073a <_ZTv0_n24_N1D4TestEv+3>:   add    -0x18(%r10),%rdi
   0x40073e <_ZTv0_n24_N1D4TestEv+7>:   jmp    0x400722 <D::Test()>

file

如前文所述,g++的实现是将虚函数表和虚基地址偏移量一起在虚表里,通过虚指针表明,虚基地址偏移量在虚表的最后一项。

详细内容可以看一下下面引用的百科。

参考

https://www.sandordargo.com/blog/2020/12/23/virtual-inheritance
https://zh.wikipedia.org/zh-sg/%E8%99%9A%E7%BB%A7%E6%89%BF

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

目录