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=0n=1 和常规值;零次用例能揭示代码是否错误假设循环体一定执行。

还要为 while 准备“初始条件假”“一轮后假”“多轮后假”和“错误推进”实验。打印每轮前后状态只用于定位,最终应删除或转成断言,避免日志改变时序并掩盖问题。

三步证明循环可终止

分步1 / 3

第一步:写出四要素

标注初始状态、继续条件、循环体与推进动作,画出每轮前后状态;确认所有继续路径都更接近条件为假。

正式节点与章专属证据

  • 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

编译与运行轨迹

同一输入下比较正常与失败路径

  1. 01建立初值
  2. 02检查继续条件
  3. 03保持循环不变量
  4. 04推进并证明剩余量减少

不变量:每轮开始时已处理区间和待处理区间边界明确,推进量让剩余工作严格减少。

故障定位与复位

一次只破坏一个前提

基线可继续:每轮开始时已处理区间和待处理区间边界明确,推进量让剩余工作严格减少。

小结

  • 循环是状态机,由初始化、继续条件、循环体与状态推进共同定义
  • for 适合计数协议,while 适合前测运行状态,do-while 明确至少执行一次
  • <<= 决定边界和次数,半开区间常与数组下标及元素个数对齐
  • break 离开最内层循环,continue 跳过本轮剩余路径;二者都要纳入终止证明
  • 嵌套循环总次数是各层次数的组合,测试要预测执行次数、首末值和结束状态

练习

  1. 问题 1:预测计数。 for (int i=0; i<5; ++i) 输出哪些值、执行几次?改成 <=5 呢?
  1. 问题 2:诊断死循环。 while 中 continue 位于 ++i 之前,为什么可能永不结束?
  1. 问题 3:选择结构。 固定打印 10 行、读到 EOF、菜单至少显示一次,分别适合什么循环?

名词解释

名词解释

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

循环
条件成立时重复执行并通过状态变化结束的控制结构。
for 循环
把初始化、条件和轮末更新集中在头部的计数结构。
while 循环
每轮前检查条件,可能执行零次的前测循环。
do-while 循环
循环体至少执行一次的后测循环。
break / continue
提前离开循环或跳过本轮剩余路径的控制语句。

资料与写作方式声明

本章以高桥麻奈 2017 年 C++ 入门教材第 5 版权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

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

讨论

评论区加载中…