Chapter 13:Class Inheritance

对齐第6版 Chapter 13:掌握 public inheritance、派生构造、静态/动态绑定、virtual function、对象切片、虚析构与 abstract base class。

学习目标

  • 能判断 public inheritance 是否满足 is-a 替换契约,并比较 private、protected 与 public 成员在派生边界的可见性
  • 能实现基类/派生类的构造、override 与 virtual destructor,复现完整对象的构造和逆序析构路径
  • 能解释 static and dynamic binding、对象切片与 pure virtual function,并设计可由基类引用驱动的多态接口

机制总览

Chapter 13:Class Inheritance:机制路径

  1. 1

    为什么复用几行代码不足以证明应该继承

    公有继承(public inheritance)承诺 Derived is-a Base:任何只依赖 Base 公有契约的代码,都应能接收 Derived 而不被惊讶。它不是“Derived 内部碰巧需要 Base 的几个成员”。若只想复用实现而不承诺替换,包含(has-a)通常更准确。

  2. 2

    派生对象包含一个完整基类子对象

    派生类声明基类后,会先构造 Base subobject,再构造派生成员,最后执行派生构造函数体。派生构造函数用初始化列表选择基类构造函数;若省略,只会尝试基类默认构造。

  3. 3

    protected 是对所有未来派生实现的承诺

    private member 只能由基类成员/friend 访问;protected member 还允许派生类实现直接访问;public member 对普通调用者开放。protected 看似方便,却把表示暴露给所有派生类,使基类难以改变布局和约束。优先保持数据 private,提供窄 protected/public operation。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

Chapter 13:Class Inheritance:机制与证据

切换《Chapter 13:Class Inheritance》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 为什么复用几行代码不足以证明应该继承

公有继承(public inheritance)承诺 Derived is-a Base:任何只依赖 Base 公有契约的代码,都应能接收 Derived 而不被惊讶。它不是“Derived 内部碰巧需要 Base 的几个成员”。若只想复用实现而不承诺替换,包含(has-a)通常更准确。

可核验证据

从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「为什么复用几行代码不足以证明应该继承」的状态变化。

学完《Chapter 13:Class Inheritance》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

Chapter 13:Class Inheritance:失效与核验

为什么复用几行代码不足以证明应该继承

典型失效

若只复述「为什么复用几行代码不足以证明应该继承」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。

核验证据

从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「为什么复用几行代码不足以证明应该继承」的状态变化。

派生对象包含一个完整基类子对象

典型失效

若只复述「派生对象包含一个完整基类子对象」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。

核验证据

从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「派生对象包含一个完整基类子对象」的状态变化。

protected 是对所有未来派生实现的承诺

典型失效

若只复述「protected 是对所有未来派生实现的承诺」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。

核验证据

从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「protected 是对所有未来派生实现的承诺」的状态变化。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么复用几行代码不足以证明应该继承

公有继承(public inheritance)承诺 Derived is-a Base:任何只依赖 Base 公有契约的代码,都应能接收 Derived 而不被惊讶。它不是“Derived 内部碰巧需要 Base 的几个成员”。若只想复用实现而不承诺替换,包含(has-a)通常更准确。

替换性要求派生 override 不加强前置条件、不削弱后置条件,并保持基类不变量。例如 Shape::area() 承诺返回有限非负面积,Circle 不能在半径为零时突然拒绝调用,也不能返回负值。

派生对象包含一个完整基类子对象

派生类声明基类后,会先构造 Base subobject,再构造派生成员,最后执行派生构造函数体。派生构造函数用初始化列表选择基类构造函数;若省略,只会尝试基类默认构造。

#include <string>
 
class Person {
public:
    explicit Person(std::string name) : name_{std::move(name)} {}
    const std::string& name() const { return name_; }
 
private:
    std::string name_;
};
 
class Student : public Person {
public:
    Student(std::string name, int cohort)
        : Person{std::move(name)}, cohort_{cohort} {}
 
private:
    int cohort_;
};

派生构造函数不能直接初始化基类 private member,而是调用基类构造函数建立其不变量。销毁顺序相反:先执行派生析构函数体与派生成员清理,再销毁基类子对象。

protected 是对所有未来派生实现的承诺

private member 只能由基类成员/friend 访问;protected member 还允许派生类实现直接访问;public member 对普通调用者开放。protected 看似方便,却把表示暴露给所有派生类,使基类难以改变布局和约束。优先保持数据 private,提供窄 protected/public operation。

class Account {
protected:
    bool applyDebit(double amount) {
        if (amount <= 0.0 || amount > balance_) return false;
        balance_ -= amount;
        return true;
    }
 
private:
    double balance_{0.0};
};

这里派生类能请求“扣款”而不能直接把 balance 写成任意值;不变量仍由基类控制。

virtual function 把调用绑定到动态类型

非 virtual 调用按表达式的静态类型在编译期绑定;virtual function 通过基类 pointer/reference 调用时,根据对象的动态类型选择最终 override,这就是多态继承(polymorphic inheritance)。

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual void draw(std::ostream& out) const = 0;
};
 
class Circle final : public Shape {
public:
    explicit Circle(double radius) : radius_{radius} {}
    double area() const override { return 3.141592653589793 * radius_ * radius_; }
    void draw(std::ostream& out) const override { out << "circle"; }
 
private:
    double radius_;
};

override 让编译器验证签名确实覆盖基类 virtual,避免 const、参数或拼写差异悄悄创建新函数。final 可表达不允许进一步派生或覆盖的设计决定。

基类引用保留身份,按值传递会切片

render(const Shape&) 绑定 Circle 时没有复制完整对象,动态类型仍是 Circle,virtual call 能分派到 Circle override。render(Shape value) 试图只复制 Base subobject,派生状态被切掉,称为 object slicing;抽象基类甚至不能按值构造。

void render(const Shape& shape, std::ostream& out) {
    shape.draw(out);             // dynamic binding
    out << " area=" << shape.area();
}
 
Circle circle{2.0};
render(circle, std::cout);

多态集合通常保存 owning smart pointer 或受控的 pointer/reference,不能保存 Base value 期待保留派生身份。借用还要保证完整派生对象活过所有调用。

先预测四种调用使用静态还是动态绑定、是否切片或禁止实例化,再切换实验面板核对。证明需要同时记录表达式静态类型、对象动态类型、成员是否 virtual。

virtual destructor 关闭完整派生生命周期

只要基类可能被多态使用并通过 Base pointer 删除,析构函数就应是 virtual。这样 delete base 先调用派生析构,再调用基类析构;否则只销毁 Base 部分是未定义行为,派生资源可能泄漏。

Shape* shape = new Circle{3.0};
delete shape;  // requires virtual Shape::~Shape

更常见的 owner 是 std::unique_ptr<Shape>,但智能指针默认 deleter 最终仍执行 delete Shape*,不能替代虚析构契约。若基类明确不允许多态删除,可把非虚析构设为 protected,阻止外部通过 Base pointer delete。

构造与析构期间动态类型逐层打开和关闭

基类构造先完成,派生层才可建立;完整生命周期中 virtual dispatch 到 most-derived override;析构则先关闭派生层。把虚调用放在构造/析构中通常意味着初始化协议不清,应改成构造后显式启动或非虚 helper。

abstract base class 定义共同协议

含 pure virtual function(= 0)的类是抽象基类(abstract base class, ABC),不能直接实例化,但可拥有数据、普通函数、构造函数和 pure virtual 的定义。ABC 应表达稳定共同协议,而不是塞入所有派生类可能需要的操作。

double totalArea(const std::vector<std::unique_ptr<Shape>>& shapes) {
    double sum = 0.0;
    for (const auto& shape : shapes) {
        sum += shape->area();
    }
    return sum;
}

具体派生类必须实现仍为 pure 的操作才能实例化。调用者依赖 Shape 接口,不需要枚举 Circle、Rectangle 等具体类型;新增类型时主要新增实现而不是修改分支。

三步验证多态层次

分步1 / 3

第一步:证明 is-a 替换性

用基类前置/后置条件测试每个派生对象,区分需要替换的 public inheritance 与只需复用的 containment。

小结

  • public inheritance 是 is-a 替换承诺;只为实现复用应优先 containment
  • 派生对象先构造基类子对象,析构顺序相反;protected 数据会扩大表示依赖
  • static binding 看表达式类型,dynamic binding 需要基类 pointer/reference 与 virtual function
  • 按 Base value 传递会 object slicing;多态接口通过引用/指针保留动态身份
  • polymorphic base 需要 virtual destructor,构造/析构期间的 virtual dispatch 只到当前层
  • abstract base classes 以 pure virtual operation 定义共同协议,不能直接实例化

练习

  1. 问题 1:选择继承或包含。 Car 需要复用 Engine 的 start/stop,实现应写 class Car : public Engine 吗?
  1. 问题 2:预测调用。 Base& ref 绑定 Derived,Base 有 virtual f 和非 virtual g,分别调用哪个版本?按 Base value 复制后呢?
  1. 问题 3:审计删除。 unique_ptr<Base> 持有 Derived,为何仍必须检查 Base 析构?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

公有继承
承诺派生对象可替代基类对象的 is-a 关系。
protected
向派生实现开放但不向普通调用者开放的访问级别。
虚函数
通过基类引用或指针按动态类型选择 override 的成员函数。
静态绑定
按表达式编译期类型决定调用目标。
动态绑定
运行时按对象动态类型选择 virtual override。
对象切片
按基类值复制时丢失派生部分和动态身份。
抽象基类
含纯虚函数、不可直接实例化的共同多态协议类。

资料与写作方式声明

本章以C++ Primer Plus, Sixth Edition, Chapter 13 Class Inheritance权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…