摘要
这节课开启本课程的第五单元,主题是函数。它说明本单元的用途:教会把一个问题分解为有名字的部分,每一部分都有接口和契约,并通过把某些问题化归为它们自身的一个更小的实例来求解,也就是递归;介绍本单元的四节课以及把它们串起来的线索;回顾本单元假定已有的知识,尤其是第 4 单元的循环及其不变式;提出本单元要解决的问题,即一个程序把同一个计算复制了一遍,又因复制时的疏忽而给出错误答案,编译器却一言不发;预先介绍契约(前置条件和后置条件),作为本单元规定每一部分规格的形式;并说明本单元通向何处,包括后面各单元以及“C++ 程序设计 II”课程。
学习目标
- 描述本单元的内容顺序,并说明每节课为后一节课提供了什么。
- 回顾第 4 单元的算法(循环的跟踪、它的不变式和它的变式),必要时加以复习。
- 借助一个例子,预先理解契约的思想:程序的一个部分对使用它的人提出什么要求,又反过来向其保证什么。
- 认清学完本单元时应当会做什么,并把它与后面各单元将要学习的内容区分开来。
入门测评
在学习本单元之前,先测一测自己的起点是有益的。这项测评是自我测评:不计分,唯一的目的是为学习指引方向。前五道题检查本单元假定已经掌握的内容:循环的跟踪、它的不变式、一个规格说明的前置条件、初始化时对值的复制,以及在第 4 单元的一个程序里找出一个有自己的输入和输出的计算。其后的五道题按课的顺序涉及本单元的核心思想:调用栈、实参的传递、计算与呈现的分离、阶乘的递归定义,以及一个调用自身的函数的执行。没有人必须现在就答对,其中有几道题里,“我还不知道”是诚实的回答:每个答案,无论对错,都会解释要点,并指出讲解这一要点的那节课。同样的能力会在单元末尾的总结课中再次测量。
开始本单元之前,先测一测你的起点。这是一次简短的自我测评:不计分,结果只保存在你的浏览器中。
第 1 题来自前面的单元
int n{305}; int c{1}; while (n >= 10) { n = n / 10; ++c; } std::println("{}", c);输出什么?只数了轮数,忘了c从 1 开始。多走了一轮:到 时条件n >= 10已经为假。把计数器与第一轮之后n的值混淆了。第 2 题来自前面的单元在循环
int total{0}; int k{1}; while (k <= n) { total += k; ++k; }中(),每次计算条件时,哪个关系成立?它在一轮的中途成立,即加法之后、自增之前,而不是在计算条件时成立。它只在结束时成立:这是后置条件,不是不变式。这是循环条件,而它恰恰在退出时为假;不变式在那时也成立。第 3 题来自前面的单元除法的规格说明是:“给定整数 和 ,求 和 ,使得 且 ”。它的前置条件是什么?
把前置条件与后置条件混淆了:这是结果所满足的性质,而不是对数据的要求。这是对输出的条件,而不是对输入的条件。当 时,不存在任何 使 成立:问题无解,而前置条件把这种情况排除在外。前置条件是为使问题有解而对数据提出的要求;后置条件是结果所满足的性质。本单元把这一对条件变成每个函数的契约:前置条件约束调用者,后置条件约束函数。复习: 从问题到算法
第 4 题来自前面的单元执行
int a{3}; int b{a}; b = 7;之后,a的值是多少?以为b和a从此永远绑在一起;初始化复制的是值,而不是存储单元。把复制与未初始化的变量混淆了:a得到了 3,之后没有任何东西改变它。第 5 题来自前面的单元第 4.4 课中输出小于 100 的素数的程序,有一个遍历候选数的循环,里面还有一个试验其因数的循环。哪一部分是有自己的输入和输出、值得起个名字的计算?
把整个程序的输入和输出与内部某个计算的输入和输出混淆了。这是一条语句,而不是有自身意义的计算:它没有别人想要使用的输入和结果。把数据与任务混淆了:变量保存计算的结果,但它本身不是计算。第 6 题一个程序从
main开始;main调用函数g,g又调用另一个函数h。在h执行期间,哪些调用正在进行?以为调用者一发出调用就消失了;实际上它在等待,以便从停下的地方继续。以为g在调用h时就结束了;它要等h把控制交还给它时才结束。这是诚实的回答:第 5.1 课会用一幅栈的图来解释它。第 7 题有函数
int f(int x) { x = x + 1; return x; },那么int a{3}; int b{f(a)}; std::println("{} {}", a, b);输出什么?以为函数修改了实参;默认情况下,它接收的是一份副本。以为对x的修改连返回值都影响不到。以为形参不能修改;其实可以,除非它被声明为const。形参x是f的一个变量,用实参的一份副本初始化:函数修改的是副本并返回 4,而a仍保留它的 3。这就是按值传递,C++ 的默认方式。复习: 参数与作用域
第 8 题一个程序读入一个日期,要输出它是星期几。哪种分解把计算与呈现分开了?
混合了两项任务:算出的结果既不能复用,也无法与预期值比较。它把任务分开了,却通过一个隐藏的作用把它们联系起来:读调用的人看不出数据从何而来。这是诚实的回答:第 5.3 课讲授如何决定要写哪些函数。这样,计算可以用数来测试,而不必看屏幕,还能为另一个以别的方式输出结果的程序所用。第 5.3 课把这一标准称为“一个函数,一项任务”。复习: 用函数进行设计
第 9 题下列哪个定义能定义 (对一切自然数 )?
没有基本情形,定义永远不会停止: 会指向 ,如此无穷无尽。它正确地定义了 (当 时),但没有定义 ,而它的值是 1。它用自身来定义 :这个定义是循环的,并没有把问题化小。第 10 题有
int f(int n) { if (n == 0) { return 0; } return n + f(n - 1); },那么std::println("{}", f(3));输出什么?以为只执行了一次调用,或者把实参与结果混淆了。以为基本情形的return 0是整条调用链的结果;它只是最后一次调用的结果。函数的名字从它的函数头起就已为编译器所知,可以在它自己的函数体中使用。f(3)等待f(2),它又等待f(1),它再等待f(0);最后这个调用返回 0,返回途中依次加上 1、2 和 3。这个函数计算的是 ,与第二题中的循环求的是同一个和。复习: 递归
正在加载内容…
已完成
登录账户以保存您的进度