Lesson 6:反复执行
以初始化、条件、循环体、推进与不变量证明循环正确且可终止。
学习目标
- 能解释循环的初始化、继续条件、循环体和状态推进如何共同保证有限执行
- 能比较 for、while 与 do-while,依据已知次数、哨兵条件和至少一次需求选择结构
- 能设计边界与终止实验,发现少一次、多一次、continue 跳过推进和嵌套循环成本问题
来源、版次与标准边界
“Lesson 6:反复执行”以 SB Creative 出版社书页核定高桥麻奈著、第 5 版、2017 年 6 月 14 日发行、ISBN 978-4-7973-9259-3;出版社页面标 596 页。CiNii Books 书目与 16 课目录记录正文 xxiii+571 页,日本国会图书馆书目记录 571 页。页数口径存在前置页差异,因此本课程不拿页码充当目录证据。
“Lesson 6:反复执行”的中文说明、示例、交互、练习和答案均为独立教学重写。公开资料只确认 16 个 Lesson 标题;本页列出的细分概念是课程教学映射,不冒充原书逐级小节。技术规则参考 ISO C++ 标准入口复核,但 2017 年教材的 Visual Studio 2017 语境不会被静默升级为 C++23;现代写法只能明确标为迁移说明。
围绕“怎样同时证明循环不会越界、不会漏项,并且一定能到达终止条件?”,本页要求保留“初值、每轮索引、循环不变量、推进量、终止度量、最后状态和迭代次数。”。若故障“continue 跳过状态推进,导致条件永远保持为真”无法在同一输入下制造首个分岔,应拒绝当前解释,而不是追加随机样例。
为什么循环首先是状态机
循环不是“把几行代码复制很多次”,而是让一组状态在每轮之后发生可预测变化,直到继续条件变为假。任何有限循环都要回答四个问题:初始状态是什么、什么条件允许下一轮、每轮做什么、哪一步让状态接近终止。
↡在继续条件满足时重复执行一段代码,并通过状态变化最终结束或显式跳出的控制结构。若只写 while (count < 5) 却从不修改 count,条件永远不变;若初值已经不满足条件,循环体一次也不执行。把循环画成状态机,比只盯着关键字更容易证明是否终止。
for 适合计数协议完整可见
for 把初始化、继续条件和更新放在同一行,适合“从某个值开始,按固定步长,直到边界”的计数循环。
for (int i = 0; i < 5; ++i) {
std::cout << i << '\n';
}执行顺序是:初始化一次;每轮前检查 i < 5;条件为真则执行循环体;随后执行 ++i;再回到条件。输出是 0 到 4,共五项。若写 i <= 5 就会多一次,是否错误取决于需求区间是半开 [0,5) 还是闭区间 [0,5]。
半开区间常让“元素个数”等于上界减下界,并与数组下标 0 到 size-1 对齐。选择哪种形式不靠习惯,靠输入域和预期次数。
while 适合次数由运行状态决定
当事先不知道要循环几次,只知道“条件仍成立就继续”,while 更自然。例如持续读取整数,直到输入失败或用户输入哨兵值。
int value = 0;
while (std::cin >> value) {
if (value == -1) {
break;
}
std::cout << "accepted: " << value << '\n';
}这里输入成功是继续前提,-1 是业务哨兵。循环可能因流失败或显式 break 结束,两种出口应分别说明;否则调用者无法区分正常结束、用户停止与输入错误。
do-while 明确至少执行一次
do-while 先执行循环体,再检查条件,因此即使初始条件为假也执行一次。它适合菜单至少展示一次、输入至少提示一次等后测协议。
int command = 0;
do {
std::cout << "1:start 0:quit\n";
std::cin >> command;
} while (command != 0);结尾条件后的分号是语法的一部分。若输入失败,command 可能保持旧值而循环继续,因此真实程序还要检查流状态;“至少一次”不等于“无条件相信每次输入”。
三种结构按协议选择
for、while 与 do-while 可以互相改写,选择标准不是能力差异,而是让循环协议最清楚:已知计数范围用 for;每轮前由外部状态决定是否继续用 while;必须先执行一次再判断用 do-while。
不应为了“代码更高级”强行使用某一种。评审者应能在一个位置看见终止条件和状态推进,并从结构本身判断零次、至少一次或固定次数语义。
break 与 continue 改变本轮路径
break 立即离开最内层循环;continue 跳过本轮剩余语句,进入下一次条件检查或 for 的更新步骤。它们能简化异常路径,也会增加出口和路径数量。
for (int i = 0; i < 10; ++i) {
if (i % 2 == 0) {
continue;
}
std::cout << i << '\n';
}在 for 中,continue 后仍会执行头部更新;在手写 while 中,若更新语句位于 continue 之后,它会被跳过,可能导致死循环。状态推进应放在所有继续路径都会经过的位置,或把计数协议交给 for 头部。
嵌套循环要计算总工作量
嵌套循环中,内层会为外层每一轮完整执行。外层 3 次、内层 4 次,循环体执行 12 次。二维表格、棋盘与矩阵天然有两个维度,但不应在不知道成本时随意增加嵌套。
for (int row = 0; row < 3; ++row) {
for (int column = 0; column < 4; ++column) {
std::cout << '(' << row << ',' << column << ") ";
}
std::cout << '\n';
}break 只退出最内层;若要停止两层,可把搜索封装为函数并 return、使用状态标志,或重新设计控制结构。方案应让退出范围一眼可见,而不是堆叠隐蔽跳转。
用不变量检查每一轮是否仍然正确
终止只证明循环会停,不证明停下时答案正确。循环不变量是在初始化后成立、每轮执行后仍成立,并能与退出条件一起推出最终结果的事实。计算前 n 个整数之和时,可把不变量写成:“进入第 i 轮前,sum 等于区间 [0,i) 中元素之和。”初始化时 i=0、空区间和为 0;每轮把第 i 项加入后再推进,事实对下一轮仍成立;退出时 i=n,所以 sum 覆盖 [0,n)。
与不变量配对的是变体量:一个有下界且每轮严格朝终止方向变化的量,例如 n-i。当 i 每轮增加且不会越过约定边界时,n-i 逐步减小到 0。若 continue 跳过 ++i,变体量不再下降,正好解释死循环根因。对 while 的输入循环,变体量未必是数字,可以是“剩余未处理输入”或“当前状态到终止状态的有限步骤”,但仍要指出哪项进展不可无限停滞。
int sum = 0;
for (int i = 0; i < n; ++i) {
sum += values[i]; // 轮末已覆盖 [0, i]
}审查循环时同时问三句:进入本轮时什么事实成立,本轮是否保持它,退出条件与该事实能推出什么。这样越界、漏项和错误累加不再只是测试偶然发现的问题,而能在结构层被指出。
把不变量写成一句可检查的话,也会直接告诉你该打印哪些状态、该为哪些边界写断言。
先预测次数和最后状态
测试循环时先预测四项:执行次数、首次值、末次值、结束后的状态。对 for (int i=0; i<n; ++i),应覆盖 n=0、n=1 和常规值;零次用例能揭示代码是否错误假设循环体一定执行。
还要为 while 准备“初始条件假”“一轮后假”“多轮后假”和“错误推进”实验。打印每轮前后状态只用于定位,最终应删除或转成断言,避免日志改变时序并掩盖问题。
三步证明循环可终止
第一步:写出四要素
标注初始状态、继续条件、循环体与推进动作,画出每轮前后状态;确认所有继续路径都更接近条件为假。
正式节点与章专属证据
- for 循环:在“Lesson 6:反复执行”中核对输入、状态变化、失败模式和可复现证据;第 1 个节点必须能回到“每轮开始时已处理区间和待处理区间边界明确,推进量让剩余工作严格减少。”。
- while 循环:在“Lesson 6:反复执行”中核对输入、状态变化、失败模式和可复现证据;第 2 个节点必须能回到“每轮开始时已处理区间和待处理区间边界明确,推进量让剩余工作严格减少。”。
- do while 循环:在“Lesson 6:反复执行”中核对输入、状态变化、失败模式和可复现证据;第 3 个节点必须能回到“每轮开始时已处理区间和待处理区间边界明确,推进量让剩余工作严格减少。”。
- 嵌套循环:在“Lesson 6:反复执行”中核对输入、状态变化、失败模式和可复现证据;第 4 个节点必须能回到“每轮开始时已处理区间和待处理区间边界明确,推进量让剩余工作严格减少。”。
- break 与 continue:在“Lesson 6:反复执行”中核对输入、状态变化、失败模式和可复现证据;第 5 个节点必须能回到“每轮开始时已处理区间和待处理区间边界明确,推进量让剩余工作严格减少。”。
先用输入合同检查本页正式节点,再在相同初值下逐步比较正常和失败轨迹,最后只启用“continue 跳过状态推进,导致条件永远保持为真”完成反例与复位。三个交互都必须能独立重置,且重置后再次满足“每轮开始时已处理区间和待处理区间边界明确,推进量让剩余工作严格减少。”。
输入与状态合同
Lesson 6:反复执行
怎样同时证明循环不会越界、不会漏项,并且一定能到达终止条件?
必须先声明
为for 循环声明输入类型、有效范围、对象生命周期和失败策略。
可复核证据
保存Lesson 6:反复执行的原始输入、初值与第一条可检查诊断。
正式节点:for 循环、while 循环、do while 循环、嵌套循环、break 与 continue
编译与运行轨迹
同一输入下比较正常与失败路径
- 01建立初值
- 02检查继续条件
- 03保持循环不变量
- 04推进并证明剩余量减少
不变量:每轮开始时已处理区间和待处理区间边界明确,推进量让剩余工作严格减少。
故障定位与复位
一次只破坏一个前提
小结
- 循环是状态机,由初始化、继续条件、循环体与状态推进共同定义
- for 适合计数协议,while 适合前测运行状态,do-while 明确至少执行一次
<与<=决定边界和次数,半开区间常与数组下标及元素个数对齐- break 离开最内层循环,continue 跳过本轮剩余路径;二者都要纳入终止证明
- 嵌套循环总次数是各层次数的组合,测试要预测执行次数、首末值和结束状态
练习
- 问题 1:预测计数。
for (int i=0; i<5; ++i)输出哪些值、执行几次?改成<=5呢?
- 问题 2:诊断死循环。 while 中
continue位于++i之前,为什么可能永不结束?
- 问题 3:选择结构。 固定打印 10 行、读到 EOF、菜单至少显示一次,分别适合什么循环?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 循环
- 条件成立时重复执行并通过状态变化结束的控制结构。
- for 循环
- 把初始化、条件和轮末更新集中在头部的计数结构。
- while 循环
- 每轮前检查条件,可能执行零次的前测循环。
- do-while 循环
- 循环体至少执行一次的后测循环。
- break / continue
- 提前离开循环或跳过本轮剩余路径的控制语句。