类的继承
2024年1月5日 2024年1月5日
大多数类都只继承自一个类, 这种形式的继承被称作单继承, 作为本章的主题.
成员函数与继承
基类的成员函数分为两种:
- 基类希望派生类进行覆盖的函数
- 基类将其声明为虚函数
virtual
, 派生类覆写时使用override
关键字 - 通过引用和指针调用虚函数时, 该调用会被动态绑定: 执行基类版本, 或某个派生类版本
- 基类将其声明为虚函数
- 基类希望派生类直接继承而不要改变的函数
虚函数限制
任何构造函数之外的非静态函数都可以是虚函数
- 构造函数不能作为虚函数
- 静态函数不能作为虚函数
虚函数的使用
-
只在声明时使用关键字
virtual
和override
- 在形参列表之后给出
override
关键字 - 如果虚函数为const成员函数,
override
关键字在const
之后 - 如果虚函数为引用, 关键字在引用限定符之后
- 如果使用尾置返回类型, 关键字在尾置返回类型之后
1void func() const & override;
- 在形参列表之后给出
-
如果基类把一个函数声明为虚函数, 则该函数在派生类中隐式地也是虚函数
直接派生类可能并不对虚函数进行覆写, 而间接派生类可能有覆写虚函数的需求
虚函数与动态绑定
成员函数如果没有被声明为虚函数, 其解析过程发生在编译时而非运行时
非虚函数的解析过程发生在编译时, 虚函数的解析过程发生在运行时
如果派生类没有覆盖基类的某个虚函数
该虚函数的行为类似于其他的普通成员, 派生类会直接继承其在基类中的版本
访问控制与继承
派生类会继承基类成员, 但不一定有访问权限
- 派生类可以访问基类的公有成员, 无法访问其私有成员
- 派生类使用
protected
访问运算符为成员设置第三种访问属性: 派生类有权访问, 而类的用户无法使用
类派生列表
- 给出基类
- 给出访问说明符, 控制派生类用户对基类公有成员的访问权限, 控制间接派生类对基类成员的访问权限
定义派生类时给出
类派生列表出现的基类, 不能只有声明, 必须已给出定义: 一个类无法派生自己 .
当一个派生非公有时
我们能使用基类的公有接口, 而无法使用派生类从基类继承的所有接口
此时无法使用动态绑定
示例
1#include <iostream> 2 3using namespace std; 4 5class FooBase 6{ 7public: 8 FooBase(int aa) : a(aa) {} 9 FooBase() : FooBase(0) {} 10 11 void printFooBase() { cout << a; } 12 13private: 14 int a; 15}; 16 17class Foo : protected FooBase 18{ 19public: 20 void printFoo() { printFooBase(); } 21}; 22 23void testFunction(FooBase &f) 24{ 25 f.printFooBase(); 26 cout << endl; 27} 28 29int main() 30{ 31 32 FooBase fb(4); 33 testFunction(fb); // 4 34 35 Foo f; 36 f.printFoo(); // 0 37 cout << endl; 38 39 testFunction(f); // 错误: 派生方式为protected, 转换失败 40 41 return 0; 42}
报错
error: cannot cast 'Foo' to its protected base class 'FooBase' testFunction(f); ^ note: declared protected here class Foo : protected FooBase ^~~~~~~~~~~~~~~~~
派生类对象
- 派生类对象包含多个组成部分: 派生类定义的(非静态)成员组成的子对象, 和与基类对应的一个或多个子对象
- 派生类对象中, 继承自基类的部分和派生类自定义的部分不一定是连续存储的
因为在派生类对象中含有预期基类对应的组成部分, 我们可以把派生类对象当成基类对象来使用, 也可以使基类的指针和引用指向/绑定到派生类对象的基类部分
派生类到基类的类型转换
derived-to-base
编译器会隐式执行派生类到基类的转换
1Quote item; 2Bulk_quote bulk; 3Quote *p = &item; 4p = &bulk; // p指向bulk的Quote部分 5Quote &r = bulk; // r绑定bulk的Quote部分
在派生类对象中含有与其基类对应的组成部分, 这一事实是继承的关键所在
派生类构造函数
- 对于从基类继承过来的成员,派生类不能直接初始化这些成员
- 派生类使用基类的构造函数来初始化基类部分
每个类控制它自己的成员初始化过程
在构造函数初始化列表中, 将实参传递给基类构造函数
1Bulk_quote(const string &book, double p, size_t qty, double disc) : Quote(book, p), min_qty(qty), discount(disc) {}
初始化基类部分
- 默认初始化
- 在构造函数初始化列表中使用基类构造函数
派生类构造函数: 首先初始化基类部分, 然后按照声明顺序依次初始化派生类成员
派生类使用基类成员
- 派生类可以使用基类非私有成员
- 派生类的作用域嵌套在基类作用域之内
遵循基类的接口
每个类负责定义各自的接口
要想与类的对象交互必须使用该类的接口: 类并不直接操作其他类的数据成员, 而是调用类的接口
初始化基类部分通过调用基类的构造函数完成
继承与静态成员
如果基类定义了静态成员, 则在整个继承体系中, 只存在该成员的唯一定义
无论被多少个派生类继承,该成员只被定义一次,只有一个实例
静态成员同样遵循通用的访问控制规则
1class Base 2{ 3public: 4 static void statmem(); 5}; 6class Derived : public Base 7{ 8 void f(const Derived&); 9}; 10void Derived::f(const Derived &derived_obj) 11{ 12 Base::statmem(); 13 Derived::statmem(); 14 derived_obj.statmem(); 15 statmem(); 16}
直接基类与间接基类
- 直接基类
direct base
出现在类派生列表中 - 间接基类
indirect base
由派生类通过其直接基类继承而来
最终的派生类包含它的直接基类的子对象以及每个间接基类的子对象
Base是D1的直接基类, 是D2的间接基类
1class Base 2{ 3 // 4}; 5class D1 : public Base 6{ 7 // 8}; 9class D2 : public D1 10{ 11 // 12};
防止继承的发生
使用 final
关键字, 表明该类不能作为基类
1class NoDerived final 2{ 3 // 4}; 5 6class Base { /**/ }; 7 8class Last final : Base { /**/ };