第16章 继承:共享布局,而不是共享分派
1. 本章是什么,以及不是什么
C++ 的继承是一个特性,两半。布局与访问那一半:派生类在最前面含有一个
基类子对象,继承它的成员,可以在任何需要基类的地方使用。另一半是通过 virtual
函数实现的动态分派。
本章只讲第一半。这里每个例子都在编译期被编译器解析完;没有一个需要 vtable,而
virtual 这个词只在一页里出现 ——
第16.7章 ——
因为 override 这个关键字没有它就毫无意义。分派那一章是第18章。
分开讲的理由是:两半的用途和代价都不同。继承布局是免费的、常见的、
自身就有用的:labeled_point 确实就是一个带名字的 point。
继承接口则是每个对象一个指针、每次操作一次间接调用,而且它就是
第15.10章类型擦除的替代方案。
把这两件事混在一起谈,正是人们最后以为“继承就是多态”的原因。
2. 每个想法的一句话总结
| 想法 | 一句话 | 章节 |
|---|---|---|
| 基类子对象 | 派生对象把自己基类对象含在自己里面,且排在最前面。 | 第16.1章 |
| 构造顺序 | 先基类,再成员,最后函数体;析构顺序完全相反。 | 第16.2章 |
| 访问说明符 | public 继承意味着 is-a;private 继承接管一个实现,
并用 using 公开选中的操作。 |
第16.3章 |
| 派生类的五法则 | 五个成员每一个都要处理基类子对象 —— 而赋值时忘掉基类也能编译通过。 | 第16.4章 |
| 转换与截断 | 派生对象就是一个基类对象,所以能绑到基类引用上 —— 也能被拷贝进基类值里,从而丢掉其余部分。 | 第16.5章 |
| 名字隐藏 | 派生类里声明的名字会隐藏基类同名的一切重载,除非你写 using。 |
第16.6章 |
override |
一个注解,让编译器去证明这个派生函数确实覆盖了基类函数。 | 第16.7章 |
| 组合 | 只要 is-a 不是字面为真,成员就比基类更好。 | 第16.8章 |
3. 贯穿全章的示例
一个小小的层次结构会走完本章:一台由基类部件与发动机组成的机器。
class machine_part
{
protected:
::std::int_least32_t serial_{};
public:
::std::int_least32_t serial() const noexcept { return serial_; }
};
class machine : public machine_part
{
protected:
::std::int_least32_t power_{};
public:
::std::int_least32_t power() const noexcept { return power_; }
};
machine is-a machine_part:每台机器都有序列号,
而任何机器都能绑定到 machine_part& 上。这里没有任何东西是虚的,
一切都在编译期决定。
4. 本章约定
-
每个函数都是
noexcept或throws,构造函数与析构函数也一样 (第6.16章)。 析构函数永远是noexcept,这在 第16.2章里有一个看得见的后果: 它不能打印,因为打印可能失败。 -
public继承一律显式写成: public base, 即使在struct里它是隐含的。那个词就是文档。 -
失败用赫布异常:
throw throws ...与catch throws (::std::error e)(第3.16章)。 -
本页所有完整程序都用
-std=c++26 -fherbceptions -lherbceptions编译并运行过;诊断信息照抄编译器的真实输出。