继承的基本概念

继承是面向对象编程中的一个核心概念,它允许一个类(子类/派生类)从另一个类(父类/基类)获得属性和方法。

核心概念

1. 基类(Base Class)/父类(Parent Class)

2. 派生类(Derived Class)/子类(Child Class)

继承的特点

1. 代码重用

2. 层次结构

3. 多态性支持

继承类型

1. 公有继承(public inheritance)

2. 保护继承(protected inheritance)

3. 私有继承(private inheritance)

访问控制

基类成员访问级别public继承protected继承private继承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
private不可访问不可访问不可访问

构造函数和析构函数

构造顺序

  1. 基类构造函数先执行

  2. 派生类构造函数后执行

析构顺序

  1. 派生类析构函数先执行

  2. 基类析构函数后执行

方法重写(Override)

派生类可以重新定义基类的方法:

Note

  • 创建、销毁的方式不能被继承 —— 构造、析构

  • 复制控制的方式不能被继承 —— 拷贝构造、赋值运算符函数

  • 空间分配的方式不能被继承 —— operator new 、 operator delete

  • 友元不能被继承(友元破坏了封装性,为了降低影响,不允许继承)

单继承下派生类对象的创建和销毁

对象创建过程

1. 构造函数调用顺序

在单继承中,派生类对象的构造遵循"从基类到派生类"的顺序:

2. 详细构造步骤

  1. 内存分配:为整个派生类对象分配内存空间

  2. 基类部分构造:调用基类构造函数初始化基类部分

  3. 成员变量初始化:按声明顺序初始化派生类的成员变量

  4. 派生类构造函数体执行:执行派生类构造函数的函数体

3. 构造函数初始化列表

派生类必须在初始化列表中调用基类构造函数(如果基类没有默认构造函数):

对象销毁过程

1. 析构函数调用顺序

析构过程与构造过程相反,遵循"从派生类到基类"的顺序:

2. 详细析构步骤

  1. 派生类析构函数体执行:执行派生类析构函数的函数体

  2. 派生类成员变量析构:按声明顺序的逆序析构派生类成员

  3. 基类析构函数调用:自动调用基类析构函数

  4. 内存释放:释放整个对象的内存空间

虚析构函数的重要性

1. 多态销毁问题

当通过基类指针删除派生类对象时,需要虚析构函数:

2. 非虚析构函数的问题

Tip

  1. 构造顺序:基类 → 派生类

  2. 析构顺序:派生类 → 基类

  3. 自动调用:基类析构函数会被自动调用

  4. 虚析构函数:多态场景下必须使用虚析构函数

  5. 异常安全:构造过程中如果发生异常,已构造的部分会被正确析构

  6. 初始化列表:派生类构造函数应在初始化列表中调用基类构造函数

对基类成员的隐藏

什么是成员隐藏

当派生类定义了与基类同名的成员时,派生类的成员会隐藏基类的同名成员。这种隐藏是基于名称的,而不是基于签名的。

隐藏的规则

1. 名称隐藏规则

2. 数据成员隐藏

访问被隐藏的成员

1. 使用作用域解析运算符

2. 使用using声明

虚函数与隐藏

1. 虚函数重写 vs 隐藏

2. 解决虚函数隐藏

完整示例

输出结果:

多重继承的派生类对象的构造和析构

多重继承中派生类对象的构造和析构比单继承更加复杂,涉及多个基类的初始化顺序、虚基类处理等问题。

构造过程

多重继承定义

基类构造顺序

多重继承中,基类的构造顺序由继承列表中的声明顺序决定,而不是初始化列表中的顺序。

  1. 内存分配:为整个派生类对象分配内存

  2. 基类构造:按继承列表顺序构造各个基类

  3. 成员变量初始化:按声明顺序初始化派生类成员

  4. 派生类构造函数体执行:执行派生类构造函数体

析构过程

析构顺序

析构顺序与构造顺序完全相反

  1. 派生类析构函数体执行

  2. 派生类成员析构:按声明顺序的逆序

  3. 基类析构:按构造顺序的逆序

  4. 内存释放

虚基类的构造和析构

虚基类的构造遵循特殊规则:

  1. 虚基类优先:所有虚基类首先构造

  2. 深度优先:按继承层次的深度优先顺序

  3. 左右顺序:同级别按继承列表顺序

  4. 最派生类负责:虚基类由最派生类直接构造

虚基类初始化

复杂继承层次示例

输出结果:

构造函数调用

1. 显式调用基类构造函数

2. 委托构造函数

Tip

  1. 构造顺序:按继承列表声明顺序,不是初始化列表顺序

  2. 析构顺序:构造顺序的完全逆序

  3. 虚基类优先:虚基类总是最先构造,最后析构

  4. 最派生类负责:虚基类由最派生类直接初始化

  5. 异常安全:构造失败时,已构造的基类会被正确析构

  6. 内存布局:多个基类子对象按顺序排列

  7. 虚析构函数:多态场景下必须使用虚析构函数

多重继承可能引发的问题

菱形继承问题(Diamond Problem)

菱形继承是多重继承中最著名的问题,当一个类通过多个路径继承同一个基类时发生。

问题

  1. 内存浪费:同一个基类被复制多次

  2. 二义性:不知道访问哪个基类副本

  3. 数据不一致:多个副本可能有不同的值

二义性问题

名称二义性

类型转换二义性

接口冲突

基类与派生类之间的转换

向上转换(Upcasting)

向上转换是指将派生类对象转换为基类类型,这种转换是安全的隐式的

向上转换的特点

  1. 自动进行:编译器自动执行,无需显式转换

  2. 类型安全:总是安全的,不会失败

  3. 支持多态:通过虚函数实现动态绑定

  4. 可能发生切片:对象拷贝时丢失派生类特有部分

向下转换(Downcasting)

向下转换是指将基类指针或引用转换为派生类类型,这种转换需要显式进行可能不安全

dynamic_cast

dynamic_cast 是最安全的向下转换方式,需要虚函数支持。

转换类型对比

转换类型安全性性能使用场景
向上转换安全多态、接口统一
static_cast不安全确定类型的向下转换
dynamic_cast安全不确定类型的向下转换
reinterpret_cast危险底层类型转换
const_cast有限安全移除const属性

派生类对象间的复制控制

派生类的复制构造函数

当派生类没有显式定义复制构造函数时,编译器会自动生成一个默认的复制构造函数,它会:

  1. 调用基类的复制构造函数

  2. 对派生类自己的成员进行逐成员复制

  1. 如果不在派生类复制构造函数的初始化列表中显式调用基类的复制构造函数,编译器会自动调用基类的默认构造函数(而不是复制构造函数)

  2. 派生类的复制构造函数只能访问基类的公有和受保护成员,不能访问私有成员

派生类的赋值运算符

当派生类没有显式定义赋值运算符时,编译器会自动生成一个默认的赋值运算符,它会:

  1. 调用基类的赋值运算符

  2. 对派生类自己的成员进行逐成员赋值

  1. 必须显式调用基类的赋值运算符,否则基类部分不会被赋值

  2. 应该检查自赋值情况(if (this != &other)

  3. 返回对象自身的引用,以支持连续赋值

Note

当派生类对象被赋值给基类对象时,会发生对象切片(object slicing):

这会导致:

  1. 只有基类部分被复制,派生类特有的成员被"切掉"

  2. 多态行为丢失,因为b是Base类型的对象,不再具有Derived的特性

多态对象的复制

当通过基类指针或引用复制多态对象时,默认的复制操作只会复制基类部分:

解决方案:虚拷贝构造模式

可以使用虚函数实现多态复制:

多重继承中的复制控制

在多重继承中,派生类的复制构造函数需要调用所有基类的复制构造函数: