七、菱形继承和虚拟继承
- 单继承:
一个子类只有一个直接父类时称这个继承关系为单继承
- 示图:
- 多继承:
一个子类有两个或以上直接父类时称这个继承关系为多继承
- 示图:
- 菱形继承:
菱形继承是多继承的一种特殊情况
- 示图:
- 菱形继承的问题:
从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题:在Assistant的对象中Person成员会有两份
- 示图:
- 示例:
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; // 主修课程 }; void Test() { // 这样会有二义性无法明确知道访问的是哪一个 Assistant a; a._name = "peter"; // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决 a.Student::_name = "xxx"; a.Teacher::_name = "yyy"; }
- 菱形虚拟继承:
虚拟继承可以解决菱形继承的二义性和数据冗余的问题
注:虚拟继承不要在其他地方使用
- 示例:
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; // 主修课程 }; void Test() { Assistant a; a._name = "peter"; }
菱形继承的内存对象成员模型:
这里可以看到数据冗余,存在二义性需要显示调用
示图:
菱形虚拟继承的内存对象成员模型:
这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,这里是通过B和C的两个指针,指向的一张表(这两个指针叫虚基表指针,这两个表叫虚基表),虚基表中第一个位置存的是当前位置距离类自己虚函数表指针的偏移量,第二个位置存的是距离继承对象的成员变量偏移量
示图:
过程示图:
总结:
对于多继承,菱形继承和菱形虚拟继承,底层实现很复杂,所以一般不建议设计出多继承,一定不要设计出菱形继承,否则在复杂度及性能上都有问题
多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承(如java)
八、继承和组合
- 概念:
- public继承是一种is-a的关系:
假设B继承了A,每个B对象就是一个A对象(每个派生类对象都是一个基类对象)
继承允许你根据基类的实现来定义派生类的实现
在继承方式中,基类的内部细节对子类可见
继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响(派生类和基类间的依赖关系很强,耦合度高)
组合是一种has-a的关系:
假设B组合了A,每个B对象中都有一个A对象(优先使用对象组合,而不是类继承 )
对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或组合对象来获得
对象组合要求被组合的对象具有良好定义的接口,因为对象的内部细节是不可见的
组合类之间没有很强的依赖关系,耦合度低,优先使用对象组合有助于你保持每个类被封装
使用总结:
实际中尽量多去用组合(组合的耦合度低,代码维护性好)
有些关系就适合继承那就用继承(强相关),另外要实现多态,也必须要继承
如果类之间的关系可以用继承,可以用组合,就用组合
示例:
// Car和BMW Car和Benz构成is-a的关系 class Car { protected: string _colour = "白色"; // 颜色 string _num = "陕ABIT00"; // 车牌号 }; class BMW : public Car { public: void Drive() { cout << "好开-操控" << endl; } }; class Benz : public Car { public: void Drive() { cout << "好坐-舒适" << endl; } }; // Tire和Car构成has-a的关系 class Tire { protected: string _brand = "Michelin"; // 品牌 size_t _size = 17; // 尺寸 }; class Car { protected: string _colour = "白色"; // 颜色 string _num = "陕ABIT00"; // 车牌号 Tire _t; // 轮胎 };
九、继承相关面试题
- 什么是菱形继承?菱形继承的问题是什么?
- 菱形继承:
一个子类继承了两个父类,而这两个父类又继承了一个相同的类,这样的继承关系如同菱形
菱形继承问题:
存在数据冗余和二义性的问题
什么是菱形虚拟继承?如何解决数据冗余和二义性的
菱形虚拟继承:
用来解决菱形继承的数据冗余和二义性
如何解决:
菱形虚拟继承会让父类生成一张虚基表,并将虚基表的地址存在其成员变量中,虚基表中存储了其父类成员变量距离该基表的距离,根据距离找到其父类变量,并且两份虚基表共同指向一份父类变量
继承和组合的区别?什么时候用继承?什么时候用组合
区别:
继承相当于每个派生类对象都是一个基类对象;继承中的基类内部实现对派生类可见(一定程度破坏基类的封装性),可以根据基类的实现来决定派生类的实现(耦合度高,不利于维护)
组合相当于每个派生类对象有一个基类对象;组合中的基类内部具体实现对派生类不可见(封装性好),基类的实现和派生类的实现依赖性低(耦合度低,利于维护)
使用情形:
如果类型之间关系符合强相关的,或者需要实现多态的,用继承
如果类型之间关系符合弱相关的,或者继承和组合都可以使用的,用组合
如何设计一个不能被继承的类?
将该类的构造函数的访问权限设置为私有,当派生类调用构造函数时,会先调用父类的构造函数,而父类的构造函数不能被调用,无法构造父类对象也就构造不了派生类对象(但这样的类不仅派生类无法构造,该类自己也不能构造)
在C++11之后,我们可以将父类用 final 修饰,这样就可以达成目的