3.3 函数调用机制

一个C++程序是由若干个函数构成的。每个函数都是独立定义的模块。函数之间可以互相调用。函数调用关系如图3.4所示。图中箭头表示在函数体内出现对另一个函数的调用。

图3.4 函数调用关系

main函数可以调用各个自定义函数和库函数,各个自定义函数可以互相调用。每个应用程序只有一个main函数,由操作系统启动。函数之间可以互相调用,可以嵌套调用。例如,在图3.4中,main函数调用函数fun2,在fun2函数体中又调用fun5,这种方式称为嵌套调用。函数可以自身调用,称为递归调用。图3.4中,fun7在函数体内出现自身调用,称为直接递归。fun4函数中调用fun1,而fun1中又调用了fun4,称为间接递归。

函数之所以能够正确地实现调用,是由于系统设置一个先进后出堆栈进行调用信息管理。执行代码出现函数调用时,系统首先把调用现场各种参数、返回后要继续执行的代码地址,压入堆栈;然后传递参数,把控制权交给被调用函数;被调用函数执行结束,堆栈弹出现场参数,控制权交还调用函数继续执行。

3.3.1 嵌套调用

函数嵌套调用的代码结构如图3.5所示。

图3.5 函数嵌套调用的代码结构

图3.5表示,在main函数中调用fa函数,在fa函数中调用fb函数,在fb函数中调用fc函数。main函数由操作系统调用,首先把操作系统的运行状态、返回地址及 main 函数的参数压入堆栈;在main函数中,调用fa,把main的运行状态、返回地址及fa的参数压入堆栈……一直到fc执行结束,堆栈弹出栈顶的第一层信息,接收fc的执行结果,恢复fb的执行现场,继续执行fb的后续代码;当fb执行结束后,堆栈又弹出顶层信息,接收fb的执行结果,恢复fa的执行现场,继续执行fa的后续代码;一直到堆栈弹空,程序执行结束。

函数调用信息管理堆栈如图3.6所示。

图3.6 函数调用信息管理堆栈

【例3-18】已知

式中,,求g(2.5, 3.4),g(1.7, 2.5)和g(3.8, 2.9)的值。

程序设计按照功能划分和代码重用的原则,首先定义f函数,通过g函数调用f函数,实现函数的完整功能。main函数向g传递实际参数的数据,完成计算。

            #include<iostream>
            #include<cmath>
            using namespace std;
            double f(double);
            double g(double, double);
            int main()
            {  cout<<"g(2.5,3.4)="<<g(2.5,3.4)<<endl;
              cout << "g(1.7, 2.5) = " << g(1.7, 2.5) << endl;
              cout << "g(3.8, 2.9) = " << g(3.8, 2.9) << endl;
            }
            double g(double x, double y)
            {  if(x<=y)  return f(x+y)/(f(x)+f(y));
              else   return  f(x-y)/(f(x)+f(y));
            }
            double f(double t)
            {  return(1+exp(-t))/(1+exp(t));  }

程序运行结果:

            g(2.5, 3.4) = 0.0237267
            g(1.7, 2.5) = 0.0566366
            g(3.8, 2.9) = 5.25325

3.3.2 递归调用

递归是推理和问题求解的一种强有力方法,原因在于,许多对象,特别是数学研究对象,具有递归的结构。简单地说,如果通过一个对象自身的结构来描述或部分描述该对象,就称为递归。

递归定义使人们能够用有限的语句描述一个无穷的集合。C++语言允许一个函数体中出现调用自身的语句,称为直接递归调用。也允许被调用的另一个函数又反过来调用原函数,称为间接递归调用。这种功能为递归结构问题提供了求解的实现手段,使程序语言的描述与问题的自然描述完全一致,因而使程序易于理解、易于维护。

下面通过一个简单的例子说明递归函数的构成规律和执行过程。

【例3-19】使用递归函数编程序求n!。

根据数学知识,非负整数n的阶乘为:

n!=n×(n-1)×(n-2)×…×1

其中,0! = 1。

n≥0时,阶乘可以用循环迭代(非递归)计算:

            fact = 1;
            for (int k = n; k >= 1; k--)
                fact *= k;

也可以用另一种递归形式定义阶乘:

阶乘的递归定义把问题分解为两部分:一部分用已知的参数n作为被乘数,另一部分使用原来的阶乘定义作为乘数。不过,乘数(n-1)! 的问题规模缩小了。

由定义有,(n-1)! = (n-1) × (n-2)!,问题规模进一步缩小,从而产生越来越小的问题,最后归结到基本情况,0!=1。C++函数调用能够识别并处理这种基本情况,向前一个函数调用返回结果,并回溯一系列中间结果,直到把最终结果返回给调用函数。

以下程序在main函数中调用求阶乘的递归函数。

            #include<iostream>
            using namespace std;
            long fact(int n)
            {  if(n==0)  return 1;                //递归终止情况
              else  return n*fact(n-1);           //递归调用
            }
            int main()
            {  int n;
              cout << "Enter n (>=0) : ";
              cin >> n;
              cout << n << "! = " << fact(n) << endl;
            }

递归函数执行由递推和回归两个过程完成。假如执行main函数时,输入n的值为3,则函数调用fact(3)的递推和回归过程如图3.7所示。

图3.7 函数调用fact(3)的递推过程和回归过程

递归调用之所以能够实现,关键是系统使用堆栈来保存函数调用中的传值参数、局部变量和函数调用后的返回地址。函数自身调用进行递推:系统把有关参数和地址压进堆栈,一直递推到满足终止条件,找到问题的最基本模式为止。然后进行回归:系统从堆栈中逐层弹出有关参数和地址,执行地址所指向的代码,一直到栈空为止,得到问题的解。

递归调用与一般函数调用,堆栈管理的操作过程是一致的。不同的是,函数的自身调用就像是产生多个正在运行(等待结束)的相同函数副本。如果这种调用不能终止并产生回归,将是程序十分严重的错误。

构成递归函数有两个基本要素:

· 描述问题规模逐步缩小的递归算法;

· 描述基本情况的递归终止条件。

· 在fact函数中,递归调用的语句为:

            t = n * fact(n-1);

由调用参数n-1,使问题规模逐步缩小,直至到达递归调用终止条件:

            n==0

返回基本情况值1。

在程序中,递归算法和递归条件通常用条件语句表达。递归调用可以出现在执行语句中,也可以出现在判断表达式中。

【例3-20】求正整数ab的最大公约数。

a和b的最大公约数,就是能同时整除ab的最大整数。从数学上可知,求ab的最大公约数等价于求ba除以b的余数(即a % b)的最大公约数。具体的算法可以用递归公式表示为:

例如,按上述递归公式求a=24与b=16的最大公约数的过程为:

因为b=24>0,

所以求a=24与b=16的最大公约数转换为求a=16与b=(24 % 16)=8的最大公约数;

因为b=8>0,

所以求a=16与b= 8的最大公约数转换为求a=8与b=(16 % 8)=0的最大公约数;

因为b=0,

所以a=8与b=0的最大公约数为8,即24和16的最大公约数是8。

按上述递归公式编写程序如下:

            #include<iostream>
            using namespace std;
            int gcd(int, int);
            int main()
            {  int a,b;
              cout << "input a (>=0) : ";
              cin >> a;
              cout << "input b (>=0) : ";
              cin >> b;
              cout << "gcd (" << a << ", " << b << ") = " << gcd(a, b) << endl;
            }
            int gcd(int a, int b)
            {  int g;
              if(b==0)  g=a;
                else  g=gcd(b,a%b);
              return g;
            }

【例3-21】斐波那契数列。

斐波那契(Fibonacci)数列的第1项为0,第2项为1,后续的每一项是前面两项的和。该数列两项的比例趋于一个常量:1.618…,称为黄金分割。数列形如:

            0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55…

斐波那契数列可以用递归定义:

Fibonacci(0) = 0

Fibonacci(1) = 1

Fibonacci(n) = Fibonacci(n-1) + Fibonacci(n-2)

根据递归定义,可以直接写出求斐波那契数列第n项的函数:

            #include<iostream>
            using namespace std;
            long fibonacci(long);
            int main()
            {  long value,n;
              cout << "Enter an integer : ";
              cin >> n;
              value = fibonacci(n);
              cout << "Fibonacci(" << n << ") = " << value << endl;
            }
            long fibonacci(long n)
            {  if(n==0||n==1)  return n;
              else   return fibonacci(n-1)+fibonacci(n-2);
            }

在main函数中调用fibonacci不是递归调用,但后续所有调用fibonacci函数都是递归的。每次调用fibonacci时,立即测试递归条件(n等于0或等于1)。如果是基本情况,则返回n。如果n>1,则递归步骤产生两个递归调用,分别解决原先调用fibonacci问题的一个简化问题。如图3.8所示为用fibonacci函数求fibonacci(3)的值的递归调用过程,图中将fibonacci缩写为fib。

图3.8 fibonacci数列的递归调用过程

前面几个例子,都可以用迭代方式来改写函数。但有一些问题,必须用递归方式解决。汉诺塔问题就是一个递归算法的经典问题。

【例3-22】汉诺塔问题。

传说印度的主神梵天在一个黄铜板上插了3根宝石针,并在其中一根针上从上到下按从小到大的顺序串上了 64 个金片。梵天要求僧侣们把金片全部移动到另一根针上去,规定每次只能移动一片,且不许将大片压在小片上。移动时可以借助第三根针暂时存放盘子。梵天说,如果这64个金片全部移至另一根针上时,世界就会在一声霹雳之中毁灭。这就是汉诺塔。如图 3.9 所示为8个金片的汉诺塔示意。

如何让计算机模拟这个游戏呢?为了便于说明,下面用精确的语言加以表述。我们称移动 n个金片的问题为n阶汉诺塔问题。以A、B、C代表3根宝石针,把金片从小到大按顺序编号为1~n,并引入记号:

            Move(n,A,B,C)

表示n个金片从A移到C,以B为过渡。

注意,要把n个金片从A移到C,必须把最大的金片n从A移到C。为此,首先要把金片1~n-1搬到B。

图3.9 8个金片的汉诺塔示意

金片n移到C后就不必再移动了。显然,一阶汉诺塔问题:

            Move(1,A,B,C)

即把一个金片直接从A移到C,是一个可以直接解决的问题,表示为:

            A→C

剩下的问题是把n-1个金片从B移到C(以A为过渡)。

于是,求解n阶汉诺塔问题变成了求解两个n-1阶汉诺塔问题,问题降了一阶。

由此可以看到,这是一个典型的递归问题。为了解决 n 阶汉诺塔问题 Move(n,A,B,C),问题可表述为:

            Move(n-1, A, C, B);
            A →C;                                 //Move(1,A,B,C)
            Move(n-1, B, A, C);

递归终止条件为 n=1。遵循降阶的步骤,经过有限步后必定能达到 n=1。这样就可以使用递归函数来求解此问题了。

            #include<iostream>
            using namespace std;
            void Move(int n,char a,  char b,  char c)
            {  if(n==1)                             //只有一个金片
                    cout << a << " --> " << c << endl;
              else                                  //否则
                  {  Move(n-1,a,c,b);               //把n-1个金片从a移到b,以c为过渡
                    cout<<a<<"-->"<<c<<endl;        //从a移一个盘子到c
                    Move(n-1,  b,  a,  c);          //把n-1个盘子从b移到c,以a为过渡
                  }
            }
            int main()
            {  int  m;
              cout << "Input the number of disks: " << endl;
              cin >> m;
              Move(m, 'A', 'B', 'C');
            }