跳至内容
Topos Uranos

摘要

到目前为止,本课程的每个程序都只是一个块,即 main,其中一切都在使用的地方写出。本课教的是如何给程序的一部分命名,并在不看其内部的情况下使用它:这就是函数。我们将学习声明,它向编译器宣告名字、返回类型和参数;定义,它添上函数体;以及调用,它把控制转交给这个函数体并接收返回值。我们将区分编译器在编译时检查的东西(每次调用都与某个声明相符)和执行时发生的事情(跳转到定义并返回)。我们将解释,为什么到达一个应当返回值的函数的末尾而没有 return 是未定义行为。我们将借助调用栈跟踪执行过程,调用栈为每个尚在进行的调用保存一个栈帧。最后,我们用契约来规定每个函数,即它的前置条件和后置条件,契约说明函数对调用者有什么要求,以及作为交换它保证什么。

学习目标

  1. 声明、定义并调用带参数和返回值的函数,并指出程序中哪一部分是声明,哪一部分是定义。
  2. 用调用栈示意图跟踪一个含有相互调用的函数的程序的执行过程。
  3. 用前置条件和后置条件规定一个函数,并写出一个违反前置条件的调用。
  4. 解释为什么在某条路径上不返回值的函数是未定义行为,并认出相应的编译器警告。

为什么需要函数

上一单元的程序都在 main 内部解决每个问题,只要问题还小,这样就够了。然而,程序一旦变大,就会出现三个困难。第一个是重复:如果同一个计算在三个地方都需要,就要写三遍,其中的一个错误也要改三遍(或者在某一处忘了改)。第二个是阅读:一个五十行的块说的不是它做什么,而是它怎么做,读者必须从机制中重建意图。第三个是测试:如果一切都混在一起,就没有办法在不执行其余部分的情况下检验某一部分。

函数是一段有名字的程序,它接收数据,称为参数,并交出一个结果,称为返回值。数学早在几个世纪前就知道这个想法:平面上两点之间的距离是它们四个坐标的函数,d(x1,y1,x2,y2)=(x2−x1)2+(y2−y1)2d(x_1, y_1, x_2, y_2) = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2},计算三角形周长的人不会把公式重复写三遍,而是给它命名并应用三次。在 C++ 中也是这样写的。

例题三角形的周长

程序读入三角形三个顶点的坐标,并输出它的周长,即三条边的长度之和。

C++ 程序
#include <cmath>#include <iostream>#include <print>​// 点 (x1, y1) 与点 (x2, y2) 之间的距离。double distance(double x1, double y1, double x2, double y2){    const double dx{x2 - x1};    const double dy{y2 - y1};    return std::sqrt(dx * dx + dy * dy);}​int main(){    double ax{};    double ay{};    double bx{};    double by{};    double cx{};    double cy{};    std::cin >> ax >> ay >> bx >> by >> cx >> cy;    const double perimeter{distance(ax, ay, bx, by) + distance(bx, by, cx, cy)                           + distance(cx, cy, ax, ay)};    std::println("周长:{}", perimeter);}
程序输入
0 0 3 0 0 4
输出
周长:12

顶点为 (0,0)(0, 0)、(3,0)(3, 0) 和 (0,4)(0, 4) 的三角形,三边长为 3、5 和 4,其和为 12。

函数 distance 写一次,用三次;main 读起来就像周长的定义,“三段距离之和”;而 distance 可以单独测试,只要用一对已知距离的点。这样就得到了原先缺少的三样东西。适用于任何语言的一般教训是:函数为一个想法命名,使用它的人想的是它做什么,而不是怎么做。这种使用与实现的分离称为过程抽象,它是整个单元的主题。