やさしい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
诊断顺序应从最早失败阶段开始:先看能否编译,再看能否链接,然后看输入路径和边界,随后检查地址/生命周期、对象分派与不变量,最后检查路径、模式、格式和流状态。越早定位,越少无关修改。
每次只改变一个条件并保留原始证据,避免多个修复同时发生后无法判断真正根因。
用故障注入做最终验收
先预测每个故障会在哪一层被拒绝,再实施修改。若非法分数进入对象、半条记录复用旧值、追加导致记录翻倍或派生对象被切片,就沿图中的课程和证据回退修复。
第一步:建立正常往返基线
从干净产物编译,固定临时绝对路径;创建两条合法记录,保存、关闭、重读并逐字段比较,记录确切文件文本。
小结
- 官方 16 课最终形成源码、构建、路径、内存、对象与文件的端到端证据链
- 条件、循环和函数先靠边界样例集证明,数组和指针再增加地址、长度与生命周期契约
- class 的核心是有效对象闸门;继承与多态还要证明替换、分派、所有权和析构
- 文件打开不是保存完成,必须以写后状态、独立重读和字段比较闭合往返
- 总复习的完成标准是能制造并定位失败,不是能背章节名称或只跑通一次正常样例
练习
- 问题 1:失败分层。 “头文件能找到但程序报 undefined reference”属于哪一层,应收集什么证据?
- 问题 2:对象与文件边界。 文件含
Mina 120时,解析器和 ScoreRecord 各负责什么?
- 问题 3:设计最终验收。 为两条记录列出正常往返和四个故障实验的最小断言。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 端到端证据链
- 从当前源码到构建、运行、对象和持久结果都可追踪复现的证明链。
- 构建证据
- 证明各源文件、目标文件和符号如何形成最终程序的命令与产物。
- 边界样例集
- 覆盖代表值、上下边界和边界外值的输入集合。
- 地址长度契约
- 数组地址、数量、半开区间和生命周期的联合访问约定。
- 有效对象闸门
- 只有完整验证通过才创建或替换对象的构造边界。
- 文件往返
- 写出关闭后独立重读,并比较数量、字段与状态的验证。