菱形继承
一、普通继承
1.本质
会继承父类的全部成员。
2.原理
在普通继承方式下,当子类继承父类的全部成员后,可能会出现重复的成员,这些成员可能来自父类的父类。
3.内存结构
对于菱形继承,D继承B和C,B和C均继承A,则D的成员变量如下所示。
- B的成员变量
- C的成员变量
- 自己的成员变量
0x007DF718 01 00 00 00 02 00 00 00 ........
0x007DF720 01 00 00 00 03 00 00 00 ........
0x007DF728 04 00 00 00 cc cc cc cc ....????
4.注意点
- 从不同路径继承来的同一基类,会在子类中存在多份拷贝,从而浪费存储空间;
- 将基类指针指向子类中对象的地址,但是多重继承可能存在一个基类的多份拷贝,从而引发二义性;
会使得对象无法确定到底是哪个类的成员变量。
二、虚继承
1.本质
解决普通的菱形继承所产生的二义性和浪费空间的问题。
在继承属性前面加上“ virtual ”关键字。
class B:virtual public A {}
2.原理
在使用虚继承的菱形继承方式下,子类继承两个父类,且两个父类均虚继承父类的父类,则会出现一个指向存储偏移地址的空间的指针,且能够解决二次项和浪费资源的问题。
3.内存结构
对于使用虚继承的菱形继承,D继承B和C,B和C均虚继承A,则D的成员变量如下所示。
- B的定位指针和成员变量;
- C的定位指针和成员变量;
- 自己的成员变量;
- A的成员变量;
0x00B5FB9C 48 8b bb 00 02 00 00 00 H??.....
0x00B5FBA4 54 8b bb 00 03 00 00 00 T??.....
0x00B5FBAC 04 00 00 00 01 00 00 00 ........
4.定位指针所指向空间的内存结构
(1)内存结构成员
第一成员——全0
第二成员——父类到 父类的父类 的偏移
0x00BB8B48 00 00 00 00 14 00 00 00 ........
(2)从对象基地址跳转到 父类的父类
需要3行汇编代码解决。
- 先取当前对象首地址到 ecx 寄存器;
- 将 ecx 寄存器里面的地址+4,取出父类到 父类的父类 的偏移;
- 将 基地址+偏移 可以从当前对象定位到父类所属区域;
mov ecx,dword ptr [Object]
mov edx,dword ptr [ecx+4]
lea eax,Object[edx]
(3)代码
从子类定位到 父类的父类 。
A* a = &Object;
三、存在虚函数的虚继承
1.本质
在已经出现虚继承的基础上,父类以及父类的父类均有虚函数。
2.原理
在使用虚继承的菱形继承方式下,子类继承两个父类,且两个父类均虚继承父类的父类,则会出现一个指向存储虚表地址和偏移地址的空间的指针。
3.内存结构
对于使用虚继承的菱形继承,D继承B和C,B和C均虚继承A,则D的成员变量如下所示。
- B的虚表地址、定位指针、成员变量;
- C的虚表地址、定位指针、成员变量;
- 自己的成员变量;
- A的虚表指针、成员变量;
0x012FF844 7c 9b 4a 00 9c 9b 4a 00 |?J.??J.
0x012FF84C 02 00 00 00 88 9b 4a 00 ....??J.
0x012FF854 a8 9b 4a 00 03 00 00 00 ??J.....
0x012FF85C 04 00 00 00 94 9b 4a 00 ....??J.
0x012FF864 01 00 00 00 cc cc cc cc ....????
4.定位指针所指向空间的内存结构
(1)内存结构成员
第一成员——当前地址到本类虚表的偏移(一般为负数)
第二成员——父类到 父类的父类 的偏移
0x004A9B9C fc ff ff ff 18 00 00 00 ?.......
(2)从对象基地址跳转到 父类的父类
需要3行汇编代码解决。
- 先取当前对象首地址到 ecx 寄存器;
- 将 ecx 寄存器里面的地址+4,取出父类到 父类的父类 的偏移;
- 将 基地址+偏移 可以从当前对象定位到父类所属区域;
mov ecx,dword ptr [Object]
mov edx,dword ptr [ecx+4]
lea eax,Object[edx]
四、构造函数的调用
1.本质
对于菱形继承,无论是否使用虚继承,都不会反复调用 父类的父类 的构造函数。
2.原理
使用汇编代码 push 0 作为调用构造函数的标记。
- 首先执行爷爷的构造函数,然后 push 0;
- 然后执行父亲的构造函数,然后 push 0;
- 之后执行叔叔的构造函数;
- 最后执行自己的构造函数;
五、析构函数的调用
1.本质
对于菱形继承,无论是否使用虚继承,都不会反复调用 父类的父类 的析构函数。
2.原理
- 首先执行自己的析构函数;
- 然后执行叔叔的构造函数;
- 之后执行父亲的构造函数;
- 最后执行爷爷的构造函数;