一.继承的概念
继承是一种可以让代码复用的机制,它在保持原有类结构的基础上进行拓展,增加方法和变量形成新的类,称为派生类。派生类继承的叫做基类。
继承定义格式
继承按照访问权限符分类
| 类成员/继承方法 | public继承 | protect继承 | private继承 |
| 基类public | 派生类的public成员 | 派生类的protect成员 | 派生类的private成员 |
| 基类protect | 派生类的protect成员 | 派生类的protect成员 | 派生类的private成员 |
| 基类private | 派生类不可见 | 派生类不可见 | 派生类不可见 |
这样做的目的是protect可以让子类进行访问的到同时保证类外不会访问到protect的成员,虽然基类private派生类不可见,但派生类仍然继承了基类的成员
基类的private成员在派生类中无论以何种方式继承都是不可见的。这种不可见性意味着基类的私有成员虽然会被继承到派生类对象中,但在语法上禁止派生类对象(无论在类内部还是外部)访问这些成员。
基类的private成员在派生类中不可访问。若希望基类成员不能被类外直接访问,但允许派生类访问,则应将其定义为protected。可见,protected访问限定符正是为继承场景而设计的。
通过总结可以发现:基类的私有成员在派生类中始终不可见。对于基类的其他成员,在派生类中的访问权限等于成员在基类的访问限定符与继承方式中的较小者,遵循public > protected > private的优先级规则。
class关键字默认使用private继承方式,struct关键字默认使用public继承方式。但最佳实践是显式声明继承方式。
实际开发中主要采用public继承,极少使用protected/private继承。因为这两种继承方式会导致继承的成员只能在派生类内部使用,不利于代码的扩展和维护。
二.基类与派生类的转化
- public继承的派生类对象可以赋值给基类的指针/引用。把派生类的基类部分切片给基类指针/引用
- 基类对象不能赋值给派生类对象
- 基类的指针/引用可以强制转换类型赋值给派生类的指针/引用。但必须是基类指针指向派生类对象才是安全的。意思就是创建基类类型的指针指向派生类,而不是创建基类类型指针指向基类后又把这个指针转给派生类指针。
三.继承中的作用域
- 继承体系里基类和派生类有独立的作用域
- 派生类与基类有同名成员,派生类成员会隐藏基类的同名成员,叫做隐藏,对于隐藏的函数可以指定类域访问
- 成员函数只要同名就会构成隐藏
四.派生类默认成员函数
- 派生类构造函数必须调用基类的构造函数用来初始化基类的成员。若基类没有默认的构造函数,则派生类必须在初始化列表初始化。
- 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝构造。
派生类的operator=必须显式调用基类的operator=来完成基类部分的复制。需要注意的是,派生类的operator=会隐藏基类的operator=,因此在调用时需要指定基类作用域。
派生类的析构函数执行完毕后会自动调用基类的析构函数来清理基类成员。这种机制确保了对象销毁时遵循先清理派生类成员、再清理基类成员的正确顺序。
派生类初始化对象先调用基类构造在调用派生类构造
析构先调用派生类析构再调用基类析构
由于多态需要析构函数构成重写,导致析构函数处理成destructor导致父类子类会隐藏父类析构函数(在基类不加virtual下)
实现不可被继承类
- 可以将类名后加final这代表此类无法被继承
- 也可以将类的默认构造函数用private 让子类无法访问,就无法被继承了
继承和友元
父类的友元不会被子类继承
继承与静态成员
基类定义一个static成员则整个继承关系中只有这一个这样的成员
五.多继承
单继承:当一个派生类仅有一个直接基类时,这种继承关系称为单继承。
多继承:若一个派生类拥有两个或更多直接基类,则称为多继承。在多继承中,对象的内存布局遵循继承顺序:先继承的基类位于内存前部,后继承的基类依次排列,派生类成员则置于最后。
菱形继承:这是多继承中的特殊情形。从对象成员模型分析可见,菱形继承会导致数据冗余和二义性问题(如Assistant对象中包含两份Person成员)。由于多继承必然存在菱形继承问题,部分语言(如Java)选择直接禁用多继承来规避此问题。因此在实际开发中,应当避免设计菱形继承结构。
虚继承
因为多继承导致棱形继承二义性所以就有了虚继承。
虚继承是C++中解决多重继承带来的"菱形继承"问题的一种机制。它通过确保基类在继承体系中只被继承一次来避免数据冗余和歧义。
在多重继承中,当派生类通过不同路径继承同一个基类时,会产生"菱形继承"问题。例如:
class Base { public: int data; }; class Derived1 : public Base { // 继承Base }; class Derived2 : public Base { // 继承Base }; class Final : public Derived1, public Derived2 { // 通过Derived1和Derived2间接继承了两个Base };这种情况下,Final类中将包含两个Base子对象,导致:
- 数据冗余 - 两份Base成员变量
- 访问歧义 - 无法直接访问Base成员,必须通过特定路径
虚继承的解决方案
使用virtual关键字声明继承关系:
class Derived1 : virtual public Base { // 虚继承Base }; class Derived2 : virtual public Base { // 虚继承Base }; class Final : public Derived1, public Derived2 { // 现在只包含一个Base子对象 };实现原理
- 虚基类指针(vbase_ptr):编译器为每个虚继承的类添加一个指针,指向共享的基类子对象
- 虚基类表(vbtable):存储虚基类偏移量信息
- 共享实例:确保整个继承体系中只有一个基类实例
示例代码
#include <iostream> class Animal { public: Animal() { std::cout << "Animal constructor\n"; } void breathe() { std::cout << "Breathing...\n"; } }; class Mammal : virtual public Animal { public: Mammal() { std::cout << "Mammal constructor\n"; } }; class WingedAnimal : virtual public Animal { public: WingedAnimal() { std::cout << "WingedAnimal constructor\n"; } }; class Bat : public Mammal, public WingedAnimal { public: Bat() { std::cout << "Bat constructor\n"; } }; int main() { Bat bat; bat.breathe(); // 没有歧义,因为只有一个Animal实例 return 0; }输出结果:
Animal constructor Mammal constructor WingedAnimal constructor Bat constructor Breathing...六.继承和组合
继承与组合的区别
继承关系
- public继承体现的是"is-a"关系,即每个派生类对象本质上都是一个基类对象。
- 继承允许基于基类实现来定义派生类,这种复用方式称为白箱复用(white-box reuse)。"白箱"指基类的内部细节对派生类可见。
- 继承会破坏基类封装性:基类的修改会显著影响派生类,两者之间存在强依赖关系,耦合度高。
组合关系
- 组合体现的是"has-a"关系,例如类B组合类A时,每个B对象都包含一个A对象。
- 组合是继承之外的另一种复用方式,通过组装对象实现更复杂功能,要求被组合对象有良好定义的接口。
- 这种复用称为黑箱复用(black-box reuse),因为对象内部细节不可见,仅通过接口交互。
- 组合类之间依赖关系弱,耦合度低,有助于保持类的封装性。
使用建议
- 优先使用组合:组合耦合度低,代码更易维护
- 继承适用场景:
- 当类之间确实是"is-a"关系时
- 需要实现多态功能时
- 权衡选择:当关系既适合继承又适合组合时,优先选择组合方式