本来是想将虚拟继承的部分写在上一篇的,但是虚拟继承的分析实在有些复杂,为了方便我自己回顾,就干脆单写一篇吧。
我们之前说过了,虚拟继承可以解决菱形继承的二义性以及数据冗余的问题,实际上它也就是因为这些问题而诞生的。
虚拟继承中的内存分布
我们就以一个结构最简单的菱形继承为例:
当不使用虚拟继承时:
class A {
public:
int a;
A()
:a(0xa)
{}
};
class B1 : public A {
public:
int b1;
B1()
:b1(0xb100)
,A()
{}
};
class B2 : public A {
public:
int b2;
B2()
:b2(0xb200)
,A()
{}
};
class C : public B1, public B2{
public:
int c1;
int c2;
C()
:c1(0xc10000)
,c2(0xc20000)
,B1()
,B2()
{}
};
int main() {
C c;
while (0);
return 0;
}
可以很直观地看到类A的成员变量a在类C的对象c中存在了两份,并且,很明显这两份又分别存储于对象c中类B1与类B2成员变量所在的空间。
也即:
当使用虚拟继承时:
class A {
public:
int a;
A()
:a(0xa)
{}
};
class B1 : virtual public A {
public:
int b1;
B1()
:b1(0xb100)
,A()
{}
};
class B2 : virtual public A {
public:
int b2;
B2()
:b2(0xb200)
,A()
{}
};
class C : public B1, public B2{
public:
int c1;
int c2;
C()
:c1(0xc10000)
,c2(0xc20000)
,B1()
,B2()
{}
};
int main() {
C c;
while (0);
return 0;
}
再次查看内存对象成员模型,可以看到,之前重复继承的类A的成员变量被存储到了对象组成的最下面,其原本的位置取而代之的是两个指针——这两个指针各指向一张表,我们将那两张表称为虚基表——所以这两个指针也被称为虚基表指针。
虚基表中存储着虚基表指针到对象中虚基类(即A)成员存储位置的偏移量。而编译器也就是根据这个偏移量找到的对象c中A的成员的存储位置。
继承的小总结
这个总结本该在上一篇的,但是,没有虚拟继承的继承是不完整的,就当是继承分了个上下篇了。
所谓的C++语法复杂,不好学,多继承就是一个体现。因为有了多继承,从而导致了菱形继承,从而有了数据冗杂以及二义性等问题,为了解决这些问题,有使用了一些复杂的手段在底层实现了虚拟继承......
多继承导致的一系列问题使得其被认为是C++的缺陷之一,C++之后的语言大多都没有多继承。哪怕C++是有多继承的,在设计过程中,也并不建议设计出多继承,并且一定不要设计出菱形继承。这将会导致一系列的问题。
继承与组合(is-a 与 has-a)
is-a 是一种继承关系,我们常说的public继承便是一种is-a关系。就像人与动物一般,每一个人必定是一个动物。每一个派生类对象都是一个基类对象。
has-a 是一种组合关系,是关联关系的一种。你可以说手机有显示屏,但你不能说手机就是显示屏或者显示屏就是手机。has-a 体现的是一种包含,是整体与部分的关系。
继承允许操作者根据基类的实现来定义派生类的实现。这种方法也会被称为白箱复用。
所谓“白箱”是针对可视性而言的:在继承体系中,基类的内部细节对于子类是可见的。继承在一定程度上破坏了基类的封装,基类实现的改变会对派生类产生很大的影响。基类与派生类之间的耦合度很高。
组合是继承之外的另一种复用手段。一些新的更复杂的功能可以通过组装与组合对象获得。对象组合要求被组合的对象拥有良好定义的接口。这种风格被称为黑箱复用。
与“白箱”相反,“黑箱”的内部细节对外部不可见。被组合的对象内部如何实现,以及其实现的具体细节与组合其的类之间没有太强的依赖关系,耦合度低,可以有效的保持类的封装。