Chapter 5:Loops and Relational Expressions
对齐第6版 Chapter 5:掌握 for、while、do while、范围 for、关系表达式、文本输入、嵌套循环与二维数组,并用不变量和推进量证明终止。
学习目标
- 能解释 for、while、do while 与范围 for 的控制契约,并按已知边界、输入状态和至少一次执行需求选择形式
- 能写出循环不变量、推进量和退出后置条件,推导半开区间与关系表达式是否一致
- 能复现 off-by-one、不终止、EOF 后复用旧字符和二维索引混用,使用边界表与状态日志定位
机制总览
Chapter 5:Loops and Relational Expressions:机制路径
- 1
为什么循环需要证明而不只是重复
循环把一段代码执行多次,也把一个小错误放大多次。正确循环必须说明:第一次测试前状态合法;每轮开始时哪些事实保持;循环体怎样推进;何时退出;退出后结果满足什么。只写“重复 count 次”无法证明数组访问、输入失败和动态边界。
- 2
关系表达式定义继续执行的集合
= == != 比较操作数并产生 bool。条件 i < count 表示 i 仍位于半开区间; i <= count 则额外包含尾后位置。先把合法集合写成区间,再选择运算符,比靠记忆“通常用小于号”更可靠。
- 3
for 循环把计数生命周期放在头部
for 循环适合初始化、继续条件和更新都围绕一个局部计数器的场景。下面的不变量是:进入第 i 轮时, total 等于 values[0..i) 的和;更新后 i 增一,未处理元素严格减少。
章级决策实验
Chapter 5:Loops and Relational Expressions:机制与证据
切换《Chapter 5:Loops and Relational Expressions》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么循环需要证明而不只是重复
循环把一段代码执行多次,也把一个小错误放大多次。正确循环必须说明:第一次测试前状态合法;每轮开始时哪些事实保持;循环体怎样推进;何时退出;退出后结果满足什么。只写“重复 count 次”无法证明数组访问、输入失败和动态边界。
可核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「为什么循环需要证明而不只是重复」的状态变化。
学完《Chapter 5:Loops and Relational Expressions》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 5:Loops and Relational Expressions:失效与核验
为什么循环需要证明而不只是重复
典型失效
若只复述「为什么循环需要证明而不只是重复」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。
核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「为什么循环需要证明而不只是重复」的状态变化。
关系表达式定义继续执行的集合
典型失效
若只复述「关系表达式定义继续执行的集合」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。
核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「关系表达式定义继续执行的集合」的状态变化。
for 循环把计数生命周期放在头部
典型失效
若只复述「for 循环把计数生命周期放在头部」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。
核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「for 循环把计数生命周期放在头部」的状态变化。
为什么循环需要证明而不只是重复
循环把一段代码执行多次,也把一个小错误放大多次。正确循环必须说明:第一次测试前状态合法;每轮开始时哪些事实保持;循环体怎样推进;何时退出;退出后结果满足什么。只写“重复 count 次”无法证明数组访问、输入失败和动态边界。
↡每次循环迭代开始或结束时都保持为真的状态关系,用于连接初始条件、循环体和最终结果。关系表达式定义继续执行的集合
< <= > >= == != 比较操作数并产生 bool。条件 i < count 表示 i 仍位于半开区间;i <= count 则额外包含尾后位置。先把合法集合写成区间,再选择运算符,比靠记忆“通常用小于号”更可靠。
signed 与 unsigned 混合比较可能先转换负数,浮点相等比较受近似影响。关系表达式的语法简单,但操作数类型和领域含义仍来自 Chapter 3 的转换契约。
for 循环把计数生命周期放在头部
for 循环适合初始化、继续条件和更新都围绕一个局部计数器的场景。下面的不变量是:进入第 i 轮时,total 等于 values[0..i) 的和;更新后 i 增一,未处理元素严格减少。
int values[4]{3, 5, 7, 9};
int total{0};
for (int i{0}; i < 4; ++i) {
total += values[i];
}若循环体修改 i、count 或容器长度,不变量需要重新证明。计数器作用域限制在 for 内可减少误用;若循环后仍需最终 i,应显式说明原因而不是扩大作用域图方便。
while 循环让外部状态驱动重复
while 循环先测试条件,可能一次也不执行,适合“只要输入成功就继续”或“只要工作队列非空就处理”。把产生新值的读取操作放进条件,能避免 EOF 后沿用旧字符。
↡表示输入结束或业务终止的特殊值;它必须与正常数据域和流失败状态清楚区分。char ch{};
int letters{0};
while (std::cin.get(ch) && ch != '#') {
if (ch >= 'A' && ch <= 'Z') {
++letters;
}
}这里有两个退出原因:流无法再产生字符,或读到业务哨兵 #。退出后若要报告原因,应分别检查;不要把 EOF 当成普通字符,也不要写 while (!cin.eof()) 后再读取。
do while 表达至少一次执行
do while 先执行循环体再检查,适合先显示菜单/读取一次,然后决定是否重复。它不适合第一次执行也需要前置条件的操作;“至少一次”必须来自业务契约,而不是为了少写一段代码。
int choice{0};
do {
std::cout << "1: run, 0: quit\n";
if (!(std::cin >> choice)) {
break;
}
} while (choice != 0);break 在输入失败时离开循环,但多个 break 会增加退出路径。每条退出都要有后置状态;若循环后必须区分用户退出与格式错误,保存原因而不是只看 choice。
范围 for 用元素遍历替代手工下标
C++11 范围 for 适合访问范围中的每个元素。for (int value : values) 复制元素,修改 value 不影响数组;for (int& value : values) 绑定可写引用;只读且避免复制可用 const auto&。
for (int& value : values) {
value *= 2;
}范围 for 减少 off-by-one,但不能自动保证在遍历时修改容器结构安全。使 vector 重分配或使迭代器失效的操作仍需查容器契约。
文本输入循环必须把读取成功纳入条件
格式化 >> 默认跳过空白,get() 可以逐字符读取并保留空格和换行。选择取决于处理单位:读取数字/单词用格式化提取,统计原始字符用 get。两者都应由读取成功驱动循环。
若读取失败后只清除状态却不移除导致失败的字符,下一次读取会再次失败,形成不终止循环。恢复协议需要识别错误、清除状态、丢弃或重新解析输入,并设置尝试次数边界。
嵌套循环分别证明每个维度
二维数组 grid[rows][cols] 是 rows 个行数组,每行含 cols 个元素。外层行下标满足 [0,rows),内层列下标满足 [0,cols);总访问次数 rows*cols 不能替代逐维边界。
int grid[2][3]{{1, 2, 3}, {4, 5, 6}};
for (int row{0}; row < 2; ++row) {
for (int col{0}; col < 3; ++col) {
std::cout << grid[row][col] << ' ';
}
std::cout << '\n';
}外层每完成一轮,整行已处理;内层不变量是当前行 [0,col) 已输出。若用一个 k 同时作为行列,方阵小样例可能掩盖错误,非方阵测试更有诊断价值。
四个故障对应四个证明缺口
故障实验先预测:哪个访问越界、哪个条件不变、EOF 后哪个旧值被复用、哪个维度首先超界。运行时可配合 sanitizer 和迭代上限,但工具报告只是定位证据,根因仍是边界/推进契约缺失。
三步验收一个循环
第一步:写初始、不变量和退出
用自然语言和区间表达进入前状态、每轮保持关系、推进量与退出后置条件,再写循环语法。
小结
- 关系表达式定义继续状态集合,操作数类型和半开区间决定边界是否正确
- for、while、do while 和范围 for 表达不同控制契约,应选择最清楚暴露不变量与推进的形式
- 循环不变量连接已处理和未处理部分,推进量证明终止,退出后置条件说明最终结果
- 文本循环必须由读取成功驱动,EOF 与业务哨兵是不同退出原因
- 嵌套循环逐维证明边界,二维总元素数不能替代行列下标契约
练习
- 问题 1:证明求和。 为
for(i=0;i<count;++i)求数组和写不变量与退出后置条件。
- 问题 2:选择循环。 逐字符读到 EOF、菜单至少显示一次、固定十个元素分别选什么形式?
- 问题 3:诊断不终止。 清除 cin fail 状态后循环仍立刻失败,缺少哪一步?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 循环不变量
- 每次迭代边界都保持为真的状态关系。
- 关系表达式
- 比较次序或相等关系并产生 bool 的表达式。
- 推进量
- 每轮严格朝有界终点变化、用于证明终止的量。
- 哨兵值
- 与正常数据域区分、表示业务终止的特殊值。
- 范围 for
- 从范围依次绑定元素、无需显式下标的循环形式。
- 嵌套循环
- 循环体包含另一循环且每层独立维护边界与推进的结构。