赞
踩
目录
单继承: 一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
因为有多继承的存在,导致出现菱形继承问题。C++语言作为编程语言老大哥(也是第一个吃螃蟹的)无法避免的的踩中了这个大坑。后来的一些面向对象编程语言也是吸取C++的经验,取消了多继承的语法,如Java等。
菱形继承: 属于多继承中的一种特殊情况
菱形继承的问题: 从下图对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。
在Assistant的对象中Person成员会有两份
- #include<iostream>
- #include<string>
- using namespace std;
- class Person
- {
- public:
- string _name;
- };
- class Student :public Person
- {
- protected:
- int _num;
- };
- class Teacher :public Person
- {
- protected:
- int _id;
- };
- class Assistant : public Student, public Teacher
- {
- protected:
- string _majorCourse;
- };
- int main()
- {
- Assistant a;
- a.Student::_name = "bruse";
- a.Teacher::_name = "peter";
- return 0;
- }

可以通过显示指定访问哪个父类的成员解决二义性问题,但是数据冗余问题依然无法解决
通过虚拟继承可以解决菱形继承的二义性和数据冗余的问题
- #include<iostream>
- #include<string>
- using namespace std;
- class Person
- {
- public:
- string _name;
- };
- class Student :virtual public Person
- {
- protected:
- int _num;
- };
- class Teacher :virtual public Person
- {
- protected:
- int _id;
- };
- class Assistant : public Student, public Teacher
- {
- protected:
- string _majorCourse;
- };
- int main()
- {
- Assistant a;
- a._name = "bruse";
- a._name = "peter";
- return 0;
- }

此时问题已经解决,但对于对象模型中数据是如何存储的,我们依然不知道!
下面将使用一个简化的菱形继承继承体系来研究虚拟继承原理
从上图可以清晰的看出,D类对象d中存在两份_a数据,存在数据冗余和二义性的问题
从上图可以看出_a数据在该对象中仅存储了一份,解决了冗余的问题。
但二义性的问题又是如何解决的呢?对象模型中存储的009a7bdc和009a7be4两串十六进制数字又有什么用呢?
D类对象d中其实存在两个指针,分别属于B域和C域,009a7bdc和009a7be4两串十六进制数字便是这两个指针的值。这两个指针也被称为虚基表指针,其指向虚基表。
虚基表中存储的是偏移量,可以通过偏移量找到唯一的_a数据。
可能会有人会发现一个问题,虚拟继承不是可以解决冗余问题吗?怎么观察上述的对象模型,使用了虚继承后怎么反而比不使用虚继承还用多上了四个字节?
解答:上述的对象模型都是通过简化的菱形继承继承体系得来的,虚基类中只存有一个int类型数据。若虚基类中存有1000个元素的int数组,不使用虚继承就会存储两份int[1000],而使用就只用存储一份。
由上面可以看出,是否采用虚继承会影响D类对象的对象模型以及数据存储方式,那么B类和C类的对象会受到影响吗?
由上图可以看出,B类对象中也同样存在虚基表指针指向记录偏移量的虚基表
但是B类对象不存在数据的冗余和二义性,为什么对象模型也会设计成如此呢?
- #include<iostream>
- #include<string>
- using namespace std;
- class A{
- public:
- int _a;
- };
- class B :virtual public A{
- public:
- int _b;
- };
- class C :virtual public A{
- public:
- int _c;
- };
- class D : public B, public C{
- public:
- int _d;
- };
- void func(B& data) {
- cout << data._b << endl;
- }
- int main()
- {
- D d;
- d._a = 2;
- d._b = 3;
- d._c = 4;
- d._d = 5;
-
- B bb;
- bb._a = 1;
- bb._b = 2;
- func(d);
- func(bb);
- return 0;
- }

当出现上述代码的情况时,B类对象可以作为参数传递给func函数,D类对象也可以发生切片作为参数传递给func函数。编译器无法分辨你所传参数是B类对象还是D类对象,为了保证调用方法的一致性,B类和D类的对象模型也保持相同的设计方式。
1.public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
2.组合是一种has-a的关系。假设B组合了A,每个B对象中都有A对象
3.继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)
术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。
派生类和基类间的依赖关系很强,耦合度高。
4.对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。
对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),
因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。
优先使用对象组合有助于你保持每个类被封装。
5.实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承也可以用组合时,优先使用组合
总的来说,尽量避免菱形继承的使用,大量使用菱形虚拟继承也会导致程序效率降低 (底层多了通过虚基表指针访问虚基表中记录的偏移量,再通过偏移量寻找公共数据的步骤),但也无伤大雅,毕竟现在的CPU性能也十分强大了。
在C++中文件IO的设计中便出现了菱形继承
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。