类的继承与派生

概述

继承意味着从祖先那里获取属性行为特征

类的继承是指一个新类从现有类中获取现有特征。从另一个角度看,从现有类创建新类的过程称为类的派生。

它们本质上是相同的,只是从不同的角度看同一过程。

继承与派生的目的

继承的目的:实现设计和代码的重用。

派生的目的:当出现新问题且原程序无法很好解决时,需要对原程序进行修改。


不同的分类标准

直接参与派生类的基类称为直接基类,基类的基类或更高层次的基类称为间接基类

一个派生类可以同时有多个基类,这种情况称为多重继承。类似地,只有一个基类的情况称为单一继承

派生类的定义

派生类定义语法:

1
2
3
4
class DerivedClassName: inheritance_mode BaseClassName1,...,inheritance_mode BaseClassNamen
{
member declarations
}

创建派生类的过程

  1. 吸收基类成员

​ 吸收基类成员后,派生类实际上包含了其基类的所有成员除了构造函数和析构函数

  1. 修改基类成员

​ 如果派生类声明了一个与基类成员同名的新成员,则派生的新成员隐藏或覆盖同名的外部成员。

  1. 添加新成员

​ 派生类添加新成员以开发功能。

访问控制

访问主要来自两个方面:首先,派生类中的新成员访问从基类继承的成员;其次,在派生类外部,通过派生类对象访问从基类继承的成员。

继承模式主要分为三种类型,其各自特征如下。

公有继承 (public)

  • 基类的公有和保护成员的访问属性在派生类中保持不变,但基类的私有成员无法直接访问

  • 派生类中的成员函数可以直接访问基类的公有和保护成员,但无法直接访问基类的私有成员。

  • 通过派生类对象访问从基类继承的成员时,只能访问公有成员

保护继承 (protected)

  • 基类的公有和保护成员在派生类中作为保护成员出现,但基类的私有成员无法直接访问

  • 派生类中的成员函数可以直接访问基类的公有和保护成员,但无法直接访问基类的私有成员。

  • 通过派生类对象,无法直接访问从基类继承的任何成员

私有继承 (private)

  • 基类的公有和保护成员在派生类中作为私有成员出现,但基类的私有成员无法直接访问

  • 派生类中的成员函数可以直接访问基类的公有和保护成员,但无法直接访问基类的私有成员。

  • 通过派生类对象,无法直接访问从基类继承的任何成员

不难看出,这三种继承方法的第二点是完全相同的,这遵循了数据共享和保护的原则。派生类对象可以在公有继承下访问公有成员,但在其他情况下无法访问以保护数据。

附上一个详细的解释

类型兼容性规则

类型兼容性规则意味着在需要基类对象的任何地方,公有派生类对象可以作为替代品。通过公有继承,派生类获得基类的所有成员,除了构造函数和析构函数,具备基类的所有功能。(保护和私有继承不适用,因为对象无法访问从基类继承的任何成员)

示例:

1
2
3
4
5
class B{...}
class D: public B {...}

B b1, * pb1;
D d1;

基于上述代码,有三种替代情况:

  • 派生类对象可以隐式转换为基类对象

    b1=d1;

  • 派生类对象可以初始化基类引用

    B &rb=d1;

  • 派生类指针可以隐式转换为基类指针

    pb1=&d1

这个兼容性规则允许我们使用相同的函数统一处理基类和公有派生类对象。也就是说,当形式参数是基类对象(引用、指针)时,实际参数可以是派生类对象或指针。这大大提高了程序效率。

派生类的构造函数和析构函数

派生类的构造函数只负责初始化派生类中新添加的成员。对于所有从基类继承的成员,初始化仍由基类构造函数完成。最后,派生对象的清理也需要添加新的析构函数。

构造函数

由于派生类无法访问基类中的许多数据成员,因此需要依赖基类构造函数。在构造派生类对象时,首先调用基类构造函数,然后初始化派生类中新添加的成员对象。

一般语法形式:

1
2
3
4
5
DerivedClassName::DerivedClassName(parameter_list):BaseClassName1(BaseClassName1_initialization_parameters),...,BaseClassNamen(BaseClassNamen_initialization_parameters)
,member_object_name1(member_object1_initialization_parameters),...,member_object_namem(member_objectm_initialization_parameters)
{
other initialization operations of derived class constructor
}

构造函数执行的一般顺序:

  1. 按照继承时声明的顺序(从左到右)调用基类构造函数。
  2. 按照在类中声明的顺序初始化派生类中新添加的成员对象。
  3. 执行派生类构造函数体中的内容。

复制构造函数

派生类在进行复制构造时也使用基类的复制构造函数。

示例:如果为派生类编写一个复制构造函数(以基类作为基类),形式为:

1
Derived::Derived(const Derived &v): Base(v){...}

这里基类使用了对派生类的引用,完全符合类型兼容性规则,即派生类对象可以用来初始化基类。

析构函数

实际上,这与构造函数的思想完全一致,最大的区别在于销毁的顺序与初始化的顺序完全相反。


派生类成员的识别与访问

范围解析运算符

“::” 是范围解析运算符,用于指定要访问的成员所在的类的名称。

如果派生类声明了一个与基类成员函数同名的新函数,即使函数参数列表不同,所有重载形式的基类中同名的继承函数都会被隐藏。

数据成员也是如此。具有相同名称的新成员将覆盖基类;如果多个继承基类重复,则会出现歧义,必须通过使用类名和范围解析运算符来识别成员。

只有在同一作用域中定义的函数才称为重载。


使用关键字可以使用其他作用域中的标识符。

虚基类

假设一个派生类从多个基类继承,而这些基类中的某些或全部又是从另一个共同基类派生的,那么在这个派生类中,将会在内存中出现相同的名称和多个副本,造成程序开销。

此时,可以将共同基类设置为虚基类,以便从不同路径继承的同名数据成员在内存中只有一个副本,并且同名函数只有一个映射。

语法形式:

1
class DerivedClassName:virtual inheritance_mode BaseClassName

虚基类及其派生类的构造函数

在整个继承关系中,所有直接或间接继承虚基类的派生类必须在构造函数的成员初始化列表中列出虚基类的初始化。

在调用虚类的构造函数时,C++编译器将指定最派生类的构造函数来调用虚基类的构造函数,因此无需担心多次重复调用。

构造类对象的一般顺序是:

  1. 如果类有直接或间接的虚基类,则首先执行虚基类的构造函数。

  2. 如果还有其他基类,则按它们在继承声明列表中出现的顺序初始化,但在构造过程中,不再执行它们的虚基类构造函数。

  3. 按照定义中出现的顺序初始化新添加的成员对象。对于类类型的成员对象,如果它们出现在构造函数初始化列表中,则执行带有指定参数的构造函数;如果没有,则执行默认构造函数;对于基本类型的成员对象,如果它们出现在初始化列表中,则使用指定值赋初值,否则不做任何操作。

  4. 执行构造函数体。

程序示例 - 使用高斯消元法求解线性方程

GitHub上的源代码

综合示例 - 个人银行账户管理程序

GitHub上的源代码