摘要
这节课开启本课程的第八个也是最后一个单元,主题是更大的程序。它说明本单元的宗旨:组织、验证和调试那些已经无法装进一个文件、也无法装进脑子里的程序;介绍本单元的三节课以及把它们串起来的线索,这条线索使课程第一课开启的循环得以闭合(规格说明、构建、验证);回顾本单元假定已有的知识,也就是整门课程;提出本单元要解决的问题,即一个单文件程序在测试它所用的数据上运行正常,却对其规格说明边界上的一个错误保持沉默;预先介绍测试与证明的区别;并说明本单元通向何处,包括结课项目和“C++ 程序设计 II”课程。
学习目标
- 描述本单元的内容顺序,并说明每节课为后一节课提供了什么。
- 借助一个例子,回顾函数声明与定义的区别、规定函数的契约,以及把多个文件变成一个程序的两个阶段(编译与链接)。
- 借助一个例子,预先认识用若干用例测试一个程序与证明它对所有容许输入都正确之间的区别。
- 认清学完本单元和本课程时应当会做什么,并把它与将在“C++ 程序设计 II”中学习的内容区分开来。
入门测评
在学习本单元之前,先测一测自己的起点是有益的。这项测评不计分:它为学习指引方向。前七道题检查本单元假定已经掌握的内容(声明与定义、前置条件、第 6 单元中一个函数的契约、链接器的消息、二分查找的用例、跟踪表,以及越出向量范围的读取);其后的三道题按课的顺序涉及本单元的核心思想,在这几道题里,“我还不知道”是诚实的回答。每个答案都会解释要点,并指出讲解这一要点的那节课;同样的能力会在总结课中再次测量。
开始本单元之前,先测一测你的起点。这是一次简短的自我测评:不计分,结果只保存在你的浏览器中。
第 1 题来自前面的单元写在
main之前的int gcd(int a, int b);这一行做了什么?混淆了声明和定义:没有函数体就没有可执行的东西,而且任何函数都不会因为写出来就执行,只有被调用时才执行。混淆了声明和调用:调用不带参数的类型。不知道编译器只需签名就能检查一次调用;缺少函数体这件事,如果始终没人写出函数体,是由链接器而不是编译器发现的。第 2 题来自前面的单元函数
toBase(n, base)的前置条件是“n >= 0且2 <= base <= 10”。谁必须保证它成立?把前置条件和输入校验混为一谈:校验是程序边界的任务,而不是每个函数的任务。以为注释会被检查;编译器会丢弃注释。没有看到契约分配了义务;没有契约,出了问题时谁也不知道是谁错了。第 3 题来自前面的单元第 6.4 课的函数
binarySearch(values, target)返回target的一个位置,若找不到则返回values.size()。它的前置条件是什么?向量为空时,循环一轮也不执行,函数返回 0,也就是values.size():这种情况已由后置条件覆盖。函数会报告值不存在;要求值必须存在会使它失去用处。有重复时,函数返回该值的其中一个位置,这正是后置条件所允许的。每一轮都与中间元素比较,舍弃区间的一半,而这只有在向量有序时才成立。对无序的数据,函数会不加任何提示地给出错误的答案。前置条件写在契约里,调用方在那里能看到它。复习: 查找与排序
第 4 题来自前面的单元一个程序编译时没有错误,但在构建可执行文件时出现
undefined reference to `tetrahedral(int)'。是哪个阶段出了问题?分不清编译器的消息和链接器的消息:前者指明一个文件和一行,后者指明目标文件和名称。程序根本没有生成:没有可执行文件,就谈不上运行。缺少#include会在编译时产生was not declared in this scope,而不是这条消息。编译器接受了每一次调用,因为它看到了声明;链接器在所有目标文件和库中寻找定义,却没有找到。多个文件与自建库一课解释了常见的原因:某个文件没有编译,或者某个声明在复制时写成了别的类型。复习: 程序、编译器与 C++
第 5 题来自前面的单元下面哪一组用例最能考验一个二分查找?
把数量和覆盖混为一谈:它从不测试值不存在的情况,也不测试边界,而错误通常就藏在那里。一个已经通过的用例,是最不可能发现新问题的用例。它们有无穷多个,或者在类型的范围内多到天文数字:测试必须有所选择。第 6 题来自前面的单元执行
int a{0}; int b{1}; for (int i{0}; i < 4; ++i) { const int t{a + b}; a = b; b = t; }之后,a和b各是多少?多算了一轮:在i < 4的条件下,循环体执行四次,而不是五次。少算了一轮,这是方向相反的差一错误。忘了t保存的是在a改变之前算出的和:在最后一轮中,a得到b的 3,而b得到的是那个和 5,而不是a的新值。第 7 题来自前面的单元为什么
std::vector<int> v{1, 2}; std::println("{}", v[2]);在本课程的选项下会停止运行,而只用-O2编译、不带检测器时,却可能输出一个任意的数?以为优化能修复错误;优化只会去掉检查,并假定程序没有违反标准。把作用弄反了:让程序停下来的,是不开优化时库的检查。这是下标的差一错误:有两个元素的向量,其下标是 0 和 1。第 8 题一个带有头文件保护的头文件定义了(而不只是声明了)
double scale(double x),没有inline,并且main.cpp和report.cpp都包含了它。构建程序时会发生什么?以为头文件保护在文件之间起作用;它是在每个翻译单元内部起作用的。把正在编译的文件和整个程序混为一谈:在每个文件内部,定义只有一个。这是诚实的回答:第 8.1 课会用链接器的真实消息来展示这一点。第 9 题一个函数通过了五百个精心挑选的自动化测试。可以断言什么?
把数量多和证明混为一谈;一个位于边界上、没有任何用例触及的错误,无论内部用例有多少都会存活下来。这些用例并不是从可能的错误中随机抽取的样本,通过的频率也不能衡量程序正确的概率。这是诚实的回答:第 8.2 课正是从这个问题讲起的。第 10 题一个断点设在循环内的
total += k;这一行上,程序第三次停在那里。高亮显示的这一行表示什么?把停下来和已经迈出的一步混为一谈。把程序停下的位置(由调试的人选定)和失效的原因混为一谈。这是诚实的回答:第 8.3 课讲解断点和单步执行。
正在加载内容…
已完成
登录账户以保存您的进度