跳转至

8. 模块设计(函数)

8.1 函数的声明、定义与调用

一个函数最好只做一件说得清的事。比如“读入数组”“找最大值”“输出结果”可以分开写,这样某一步出错时也知道该查哪一段。

double max(double a, double b);     /* 声明,也叫函数原型 */

double max(double a, double b)      /* 定义 */
{
    return a > b ? a : b;
}

编译器看到函数调用之前,至少要先见过函数声明。void 函数不返回结果;其他函数走到正常出口时要有对应的 return。执行 return 后,当前函数剩下的语句不会再跑。

8.2 参数传递

C 只有值传递。调用函数时,实参的值被复制给形参;函数里改的是副本,外面的变量不会跟着变。下面第一个 swap 看着像交换,实际什么也没交换:

void swap_wrong(int a, int b)
{
    int t = a;
    a = b;
    b = t;               /* 只交换了副本 */
}

void swap(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

平时说的“传地址”,本质上仍然是值传递,只不过这次复制的值恰好是地址。函数通过地址找到原变量,才真的改到外面。

8.3 作用域、存储期与链接

  • 块内定义的名字只能在这一层花括号内使用;函数外定义的名字具有文件作用域;
  • 普通局部变量每次进入函数都会重新产生,离开后失效;
  • static 局部变量只初始化一次,下次调用函数时还保留上次的值;
  • 全局变量在整个运行期间都存在,但用得太多会让函数之间互相牵连;
  • 文件作用域的 static 函数或变量只在本源文件可见;
  • extern 用来声明“这个外部名字定义在别处”。

课件把作用域归纳为全局、文件、函数和复合语句几层。做读程序题时,先看变量定义在哪一层花括号里,再判断当前语句究竟用的是哪一个同名变量。

内外层出现同名变量时,里层会把外层遮住。还要特别留意:普通局部变量在函数返回时就没了,不能把它的地址返回出去。

8.4 递归

递归先别急着在脑中一层层展开,先找两件事:什么时候停,以及问题规模怎样变小。例如阶乘在 n <= 1 时直接返回,否则把问题交给 factorial(n - 1)

long long factorial(int n)
{
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

每层递归都有自己的一套形参和局部变量。读递归程序时,可以从最小输入开始画调用过程,再逐层把返回值带回来。层数太深会耗尽调用栈。

Hanoi 塔的分法很典型:先把 \(n-1\) 个盘移到辅助柱,移动最大的盘,再把那 \(n-1\) 个盘移到目标柱。三步里第一步和第三步与原问题同形,只是规模少了 1。

评论