标签: 虚继承

  • 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