菱形继承
菱形继承:菱形继承的问题在于数据冗余和二义性。
将子类转化成爷爷类的时候会报错,因为不知道要转化到哪个类上去,这里需要加入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()>
如前文所述,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