06-8 菱形继承
菱形继承的概念:存在着两个派生类(类B
和类C
)继承同一个基类(类A
),同时存在着另外一个类(类D
)同时继承这两个派生类。这种继承称之为菱形继承,又称之为钻石继承。
我们可以举个例子,这样可能好理解一点:
-
有一个父类,叫做动物,这个类下面有两个类,一个类是羊,另外一个类是驼。这个很好理解,毕竟两个动物嘛......之后又存在一个类,继承羊和驼,这个动物是草泥马(羊驼)......
这样是不是好理解一点了......
菱形继承可能会出现的问题:
- 类
D
会继承类B
和类C
的数据,而类B
和类C
又继承了类A
的数据。当类D
使用数据的时候,就会产生二义性。 - 类
D
继承了两份类A
的数据,但是我们应该清楚,类A
的数据我们只需要继承一份就可以了。
所以我们来看一眼
-
菱形继承
class Animal { public: int m_Age; }; class Sheep :public Animal { public: }; class Camel :public Animal { public: }; class Alpaca :public Sheep, public Camel { public: };
-
菱形继承产生二义性
#include
using namespace std; class Animal { public: int m_Age; }; class Sheep :public Animal { public: }; class Camel :public Animal { public: }; class Alpaca :public Sheep, public Camel { public: }; int main() { class Alpaca a; //a.m_Age = 100; return 0; } 在这里,我们实例化了一个类
Alpaca
的对象a
。此时,我们想对a
的成员属性m_Age
赋值,会出现错误。a.m_Age = 100;
会产生报错,报错理由如下:"Alpaca::m_Age" 不明确
这就是多继承导致的,解决方法也很简单,只要添加作用域即可。
class Animal { public: int m_Age; }; class Sheep :public Animal { public: }; class Camel :public Animal { public: }; class Alpaca :public Sheep, public Camel { public: }; int main() { class Alpaca a; a.Sheep::m_Age = 10; a.Camel::m_Age = 20; cout << "a.Sheep::m_Age = " << a.Sheep::m_Age << endl; cout << "a.Camel::m_Age = " << a.Camel::m_Age << endl; return 0; }
运行结果:(环境:Windows11(arm/Apple M VM)/Visual Studio 2022/Debug/arm64)
a.Sheep::m_Age = 10 a.Camel::m_Age = 20
当菱形继承的时候,两个父类拥有相同的数据,需要添加作用域用于区分。
-
菱形继承导致了多次继承
如上面的例子,对象
a
继承了两次m_Age
,但是实际上我们只需要一份。这样不仅会导致对线指向不明确,也会造成资源的浪费。我们先看一下修改之前的内存对象模型:
class Alpaca size(8): +--- 0 | +--- (base class Sheep) 0 | | +--- (base class Animal) 0 | | | m_Age | | +--- | +--- 4 | +--- (base class Camel) 4 | | +--- (base class Animal) 4 | | | m_Age | | +--- | +--- +---
我们尝试解决一下:
利用虚继承可以解决菱形继承的问题。
虚继承关键字是:
virtual
,处理方式是:在继承之前加上关键字virtual
,可以将这个继承变为虚继承。这时候,被继承的类(也就是原来的基类),被称之为:虚基类。
class Animal { public: int m_Age; }; class Sheep :virtual public Animal { public: }; class Camel :virtual public Animal { public: }; class Alpaca :public Sheep, public Camel { public: };
在这里,我们的类
Sheep
和类Camel
都虚继承自类Animal
。我们这时候来看一眼类
Alpaca
的内存对象模型:class Alpaca size(12): +--- 0 | +--- (base class Sheep) 0 | | {vbptr} | +--- 4 | +--- (base class Camel) 4 | | {vbptr} | +--- +--- +--- (virtual base Animal) 8 | m_Age +---
在这里,类
Sheep
和类Camel
继承的内容变为了:{vbptr}
,这叫做:虚基类指针。(VirtualBasePointer
,简称为vbptr
)。虚基类指针指向vbtable
,这叫做:虚基类表(VirtualBaseTable
,简称为vbtable
)。在这个表里面,记录的是数据的偏移量。无论是类Sheep
还是类Camel
,继承的是一个指针,这个指针指向虚基类表,这个表里记录数据的偏移量。通过这个表,可以找到唯一的数据。简单来说,使用虚继承,实际上,只有一份真实的数据。
这个在后续多态部分会详细学习。
我们再来试一下:
#include
using namespace std; class Animal { public: int m_Age; }; class Sheep :virtual public Animal { public: }; class Camel :virtual public Animal { public: }; class Alpaca :public Sheep, public Camel { public: }; int main() { class Alpaca a; a.m_Age = 12; cout << "a.Sheep::m_Age = " << a.Sheep::m_Age << endl; cout << "a.Camel::m_Age = " << a.Camel::m_Age << endl; cout << "a.m_Age = " << a.m_Age << endl; return 0; } 运行结果:
a.Sheep::m_Age = 12 a.Camel::m_Age = 12 a.m_Age = 12
- 总结:
- 菱形继承是存在着两个派生类(类
B
和类C
)继承同一个基类(类A
),同时存在着另外一个类(类D
)同时继承这两个派生类。 - 菱形继承会出现二义性。我们解决二义性的方式是添加作用域。
- 菱形继承可能会出现多次继承,我们需要使用虚继承(
virtual
)的方式处理。 - 虚继承的原理是:继承一个虚基类指针(
vbptr
),虚基类指针(vbptr
)指向虚继承表(vbtable
),虚继承表(vbtable
)记录数据偏移量,最后找到数据所在的内存空间地址,访问到真实的数据。
- 菱形继承是存在着两个派生类(类
:-)