やさしいC++ 第5版:16 课总复习

用成绩记录文件往返项目串联官方 16 课,按编译、路径、内存、对象和 I/O 失败层定位证据并完成综合验收。

学习目标

  • 能解释官方 16 课如何共同形成“源码到持久记录”的端到端证据链
  • 能复现并区分编译、链接、控制流、内存、对象和文件 I/O 六类失败,选择对应证据定位
  • 能设计并实现一个保持对象不变量的成绩记录往返项目,覆盖正常、边界与故障场景

为什么总复习要从失败反推证据

逐课学习时,可以把注意力集中在一个概念;综合程序失败时,现象会跨课传播。文件里出现重复记录,可能是 ios::app 模式,也可能是循环多执行一次;派生对象行为消失,可能是未写 virtual,也可能是按基类值复制发生切片。总复习的任务是建立诊断顺序,而不是再读一遍摘要。

第一组证据:源码、构建与值

Lesson 1–4 要回答三件事:执行的是否是当前源码,输入是否被目标类型正确读取,表达式是否按预期分组和转换。保留完整编译命令、警告和输入;删掉旧可执行文件后重建,才能排除旧产物。

Lesson 10 把这条链扩展到多文件:调用点只需声明即可编译,但最终链接必须找到唯一匹配定义。编译错误和链接错误处于不同阶段,不能用同一种 include 修法。

第二组证据:路径、边界与函数契约

Lesson 5–7 要先枚举输入集合,再实现条件、循环和函数。成绩范围可分为非法、0–59、60–89、90–100;循环每轮必须消费一条新记录或推进下标;函数要说明输入合法域、返回值和失败方式。

std::string grade(int score) {
    if (score < 0 || score > 100) return "invalid";
    if (score >= 90) return "A";
    if (score >= 60) return "pass";
    return "retry";
}

先预测 -1, 0, 59, 60, 89, 90, 100, 101 的结果,再运行断言。若函数只在 85 分通过,不能证明边界无空洞。

第三组证据:地址、长度与数据形状

Lesson 8–9 要同时记录对象生命周期与数组长度。裸指针只说明地址,不携带数组实际长度;数组参数会调整为首元素指针。任何遍历都要给出半开区间 [0,count),任何返回地址都要证明目标对象在使用时仍存在。

Lesson 11 决定记录的形状:同时存在的姓名和分数用 struct/class;有限状态用 enum;互斥表示才考虑带标签 union。类型选择应反映不变量,而不是只求占用字节少。

第四组证据:对象从构造到销毁保持合法

Lesson 12–13 用 private 状态、领域操作和构造函数守住分数范围;析构只释放当前对象直接拥有的资源。Lesson 14–15 要证明派生对象能替换基类,override 经基类引用/指针分派,经基类指针删除时基类析构为 virtual。

class ScoreRecord {
public:
    ScoreRecord(std::string name, int score)
        : name_{std::move(name)}, score_{score} {
        if (name_.empty() || score_ < 0 || score_ > 100) {
            throw std::invalid_argument{"invalid score record"};
        }
    }
 
    const std::string& name() const { return name_; }
    int score() const { return score_; }
 
private:
    std::string name_;
    int score_;
};

不要为了展示继承而制造 HonorStudent 改写分数标准:若两种记录不满足同一替换契约,组合或显式策略更准确。继承是语义关系,不是总代码复用工具。

第五组证据:文件往返闭合全书

Lesson 16 把内存状态写到外部世界。路径必须可定位,覆盖/追加模式必须明确,字段格式与范围必须固定,每次提取由流状态决定。写入成功后关闭并重新打开,把内容解析成新对象,再比较记录数和字段。

bool save(const std::filesystem::path& path,
          const ScoreRecord records[], std::size_t count) {
    std::ofstream output{path};
    if (!output) return false;
    for (std::size_t i = 0; i < count; ++i) {
        output << records[i].name() << ' ' << records[i].score() << '\n';
    }
    return static_cast<bool>(output);
}

从现象回到正确 Lesson

诊断顺序应从最早失败阶段开始:先看能否编译,再看能否链接,然后看输入路径和边界,随后检查地址/生命周期、对象分派与不变量,最后检查路径、模式、格式和流状态。越早定位,越少无关修改。

每次只改变一个条件并保留原始证据,避免多个修复同时发生后无法判断真正根因。

用故障注入做最终验收

先预测每个故障会在哪一层被拒绝,再实施修改。若非法分数进入对象、半条记录复用旧值、追加导致记录翻倍或派生对象被切片,就沿图中的课程和证据回退修复。

分步1 / 3

第一步:建立正常往返基线

从干净产物编译,固定临时绝对路径;创建两条合法记录,保存、关闭、重读并逐字段比较,记录确切文件文本。

小结

  • 官方 16 课最终形成源码、构建、路径、内存、对象与文件的端到端证据链
  • 条件、循环和函数先靠边界样例集证明,数组和指针再增加地址、长度与生命周期契约
  • class 的核心是有效对象闸门;继承与多态还要证明替换、分派、所有权和析构
  • 文件打开不是保存完成,必须以写后状态、独立重读和字段比较闭合往返
  • 总复习的完成标准是能制造并定位失败,不是能背章节名称或只跑通一次正常样例

练习

  1. 问题 1:失败分层。 “头文件能找到但程序报 undefined reference”属于哪一层,应收集什么证据?
  1. 问题 2:对象与文件边界。 文件含 Mina 120 时,解析器和 ScoreRecord 各负责什么?
  1. 问题 3:设计最终验收。 为两条记录列出正常往返和四个故障实验的最小断言。

名词解释

名词解释

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

端到端证据链
从当前源码到构建、运行、对象和持久结果都可追踪复现的证明链。
构建证据
证明各源文件、目标文件和符号如何形成最终程序的命令与产物。
边界样例集
覆盖代表值、上下边界和边界外值的输入集合。
地址长度契约
数组地址、数量、半开区间和生命周期的联合访问约定。
有效对象闸门
只有完整验证通过才创建或替换对象的构造边界。
文件往返
写出关闭后独立重读,并比较数量、字段与状态的验证。

讨论

评论区加载中…