摘要
循环一课教了如何写出重复;这一课教的是如何把它写对,并且知道它为什么是对的。本课从构成大多数循环的模式讲起(计数器、累加器、最大值与最小值、哨兵和标志),每一种模式都由它的变量在每一轮之后的含义来定义。这一观察引出核心概念:不变式,即关于变量的一个断言,它在第一轮之前和每一轮之后都成立,并且与退出条件一起蕴涵结果。这是对轮数的归纳,借助它,循环可以倒过来设计:先写最后应当成立的东西,再写不变式,最后才写循环体。终止性用变式来论证,变式已在从问题到算法一课中介绍过;本课还展示三个不终止的循环:一个使用实数条件,一个使用无符号计数器,还有一个的有符号计数器发生溢出,那是未定义行为。最后是与 Visual Studio Code 调试器的初次接触,它让程序停在某一行,并一轮一轮地显示程序的变量。
学习目标
- 在循环中识别计数器、累加器、最大值和哨兵这些模式,并用它们设计新的循环。
- 写出一个简单循环的不变式,并确认它在任意一轮中都得到保持。
- 用一个递减且有界的整数量来论证循环的终止性。
- 用 Visual Studio Code 的调试器,借助断点和变量视图,跟踪循环的执行。
模式
变量的含义
大多数循环不是发明出来的:它们由少数几种在所有程序中反复出现的部件拼装而成。每个部件都是一个变量,在循环之前有初始化,在循环之中有更新,更重要的是有一个在每一轮之后都成立的含义。按含义而不是按形式来认识这些部件,才能把它们组合起来而不出错。
- 计数器的值是已经处理了多少个数据(或其中有多少个满足某个条件)。它初始化为 0,因为在第一轮之前一个数据也没有处理;它用
++count更新,并且只在有东西可数时才更新。 - 累加器的值是已处理数据的组合:它们的和、它们的积。它初始化为运算的单位元(求和为 0,求积为 1),因为没有任何数据时,和是 0,积是 1。
- 最大值的值是已处理数据中最大的那个,最小值则是最小的那个。没有任何数据时,最大值并不存在:所以它用第一个数据来初始化,而不是用一个随意选取的数。
- 哨兵是输入中的一个值,它不是数据,而是数据已经结束的信号。这一模式要求在循环之前读一次,在循环体末尾再读一次,就像循环一课中累加到零为止的做法那样,这样条件会在每个值被处理之前先检查它,哨兵也永远不会被当作数据计入。
- 标志是一个
bool,它表示在已处理的数据中某件事是否至少发生过一次。它初始化为false,在这件事第一次发生时变为true;之后的任何一轮都不会把它改回false。
一个包含所有部件的例子
一份降雨记录记下每天降雨的毫米数。程序先读入一个阈值,然后读入若干天的测量值,以一个负数结束;负数不可能是测量值,正好用作哨兵。程序写出测量值的个数、它们的平均值、最大值、超过阈值的天数,以及是否有过无雨的日子。
#include <iostream>#include <print>int main(){ double threshold{}; std::cin >> threshold; int count{0}; // 计数器:已处理的测量值 int aboveCount{0}; // 计数器:超过阈值的测量值 double total{0.0}; // 累加器:测量值之和 double largest{0.0}; // 最大值:仅当 count > 0 时有意义 bool dryDay{false}; // 标志:出现过无雨的日子 double value{}; std::cin >> value; while (std::cin && value >= 0.0) { if (count == 0 || value > largest) { largest = value; } ++count; total += value; if (value > threshold) { ++aboveCount; } if (value == 0.0) { dryDay = true; } std::cin >> value; } if (count == 0) { std::println("没有测量值。"); } else { std::println("测量值:{} 个;平均:{:.2f} mm", count, total / count); std::println("最大值:{:.1f} mm;超过 {:.1f} mm 的天数:{}", largest, threshold, aboveCount); std::println("是否有无雨的日子:{}", dryDay ? "是" : "否"); }}104.5 0 12.5 21.5 7.5 -1测量值:5 个;平均:9.20 mm最大值:21.5 mm;超过 10.0 mm 的天数:2是否有无雨的日子:是有四个决定值得说明。条件 count == 0 || value > largest 不用单独读取,就用第一个数据初始化了最大值:在第一轮中计数器为 0,赋值总会发生;在之后的各轮中,只有当数据超过最大值时才发生。比较 value == 0.0 是精确的,因为这个值是读入的,而不是计算出来的:数字 0 会被转换成精确的零,而浮点数一课对 == 的警告针对的是运算的结果。循环条件包含 std::cin,所以没有哨兵就结束的输入也会使循环终止。最后,只有在有测量值时才计算平均值:当 count 等于 0 时,total / count 将是 ,这在浮点运算中不是错误,而是“不是一个数”这个值,程序会不加任何提示地把它写出来。
证明
循环之前:阈值为 10,两个计数器和累加器都为 0,标志为假,并且第一个测量值 4.5 已经读入。最大值为 0,但只要
count还是 0,这个值就没有任何意义。第一轮:
count为 0,所以 4.5 成为最大值;计数器增加到 1,总和增加到 4.5。读入 0。第二轮:0 没有超过最大值,但它把标志立了起来。读入 12.5。
第三轮:12.5 是新的最大值,并且超过阈值:
aboveCount增加到 1。读入 21.5。第四轮:21.5 是新的最大值,也超过阈值。读入 7.5。
第五轮:7.5 既不改变最大值,也不改变
aboveCount;总和达到 46。读入 。条件检查 ,它是负数:循环没有处理它就终止了。平均值为 。
请注意,在图的每一行中,每个变量相对于到那时为止已处理的测量值都符合它的含义:total 是它们的和,largest 是它们的最大值,dryDay 说明其中是否有过零。这个在每一轮之后都成立的性质,有一个名字。
正在加载内容…
已完成
登录账户以保存您的进度