4. 函数调用过程机制
为了理解函数机制,以下图所示的fact.c
程序为例,进行说明。
它包括前文所述的计算n!的Factorial
函数和一个显示阶乘表的主程序:
4.1 参数传递
C语言是如何使同一个名字有不同的值?又如何使得同一个概念值用不同的名字表示?
为了理解这个问题的答案,有必要介绍一下函数调用中的实际参数值和函数中用来保存这个值的变量在语义上的区别。
当主程序调用Factorial
(它本身是作为函数printf
的实际参数)时, 实际参数是表达式i
。
当这个语句被执行时, 其效果相当于检查i
的当前值, 并把它传给函数Factorial
,它的原型为:
int Factorial(int n);
原型中声明了一个整型变量n
——该变量作为实际参数的占位符。在函数头部定义的作为占位符的变量被称为形式参数(formal parameter)。
当函数被调用时,将执行下列步骤:
(1) 计算每个实际参数的值作为调用程序操作的一部分。
因为实际参数是表达式,所以这个计算可以包括运算符和其他函数。
(2) 每个实际参数值被复制到对应的形式参数变量中。
如有多个实际参数,必须按次序将其复制到形式参数。
如有必要,在实际参数值和形式参数之间要执行自动类型转换,就如在赋值语句中一样。例如,如果int类型的值要传给参数声明为double类型的函数中, 在把值复制到形式参数变量中之前,必须把该整型数转换为等价的浮点数。
(3) 执行函数体中的语句, 直到遇见return语句为止。
(4) 计算return中的表达式,如有必要,将表达式的值转换为函数指定的返回类型。
(5) 在函数调用的地方用返回值替代,继续执行调用程序。
每次调用一个函数,都会产生一组变量。
当变量以方框表示时,所有的方框都被包括在表示函数main
的大方框中, 这是程序中仅有的一个函数。
如果要对一个有多个函数的大程序进行跟踪,那么每次在一个函数调用另一个函数时,都要画一组新的变量方框。
就如在函数main
中一样,该函数中声明的每一个变量(包括形式参数) 都必须有一个方框。这些变量仅在声明它们的程序中有意义,因此被称为局部变量(local variable)。
例如,在fact.c
中的main
函数运行时,首先需要为main
函数中的变量创建空间。
函数main
只声明了一个变量(循环变量i
),因此main
函数的变量可表示为如下图所示:
变量框外的双线将所有与特定函数调用相关的变量围起来。这个变量集称为该函数的帧(frame) 或栈帧(stack frame)。
假设在程序中LowerLimit
被定义为0,则for循环的第一个周期中i
的值为0。
可以把这个值放在帧中对应的变量框main里以表示这个条件,如下图所示:
然后main
函数调用printf
,作为计算传递给函数printf
的实际参数的一部分,计算表达式
Factorial(i)
的值。
为了在帧图中表示计算机的动作,可以从查看当前帧中的变量i
的值开始,在当前帧中可以发现这个值为0。
然后为Factorial
建立一个新帧,其中值0是第一个(也是仅有的一个) 实际参数。
Factorial
函数有三个变量:形式参数n
,局部变量product
和i
。
因此Factorial
帧中有三个变量单元,如下图所示:
接下来的第一件事情是形式参数用实际参数值(即0)进行初始化。
则帧的内容如下图所示:
当为Factorial
创建一个新帧时,main
的帧没有完全去掉。该帧暂时被放在一边,直到Factorial
的操作结束为止。
为了在用方框图表示的概念模型上表示这种情况,最好将每个新帧图画在一张索引卡上,然后将新帧放在表示原来的帧的卡上,即覆盖在它上面。
例如, 当调用函数Factorial
时,表示Factorial
帧的索引卡放在了main
帧上面,如下图所示:
整个帧集合形成了一个栈,最近的帧放在最上面,这就是栈帧这个术语的来源。
一旦Factorial
被激活, main
的帧仍然存在,只是你看不到它的任何内容。
特别要注意,名字i
不再引用main
函数中声明的变量,而只引用Factorial
中的变量名i
。
下一步是执行Factorial
的函数体,方法是在当前的帧上运行函数的每一步。
变量product
被初始化为1,程序到达for循环。
在for循环中,变量i
被初始化为1,但由于该变量值已经大于n
,所以for循环体没有被执行。
因此,当程序到达return语句时,帧的内容变为:
当Factorial
返回时,product
的值作为函数的结果值传回调用程序。
从函数返回也意味着扔掉它的帧,使main
中的变量重新显现出来,如下图所示:
然后,结果值1被传给printf
函数。printf
函数经历同样的过程。
最终,printf
将结果值1显示在屏幕上,然后返回,之后main
函数继续执行for循环的下一周期。
4.2 在其他函数中调用函数
函数的主要作用在于一旦一个函数被定义,不仅主程序可以使用它,而且可以作为实现其他函数的工具,使得这些函数可以用作为更复杂的工具。
这些函数再被其他函数调用,以此类推,从而创建出具有任意复杂度的层次结构。
举例来说,要解决以下问题:
从n个不同物体的集合中选出k个物体有多少种不同的方法?
这个结果可以表示为n和k的函数,该函数称为组合函数(combination function)。可以用阶乘来定义:
C ( n , k ) = n ! k ! × ( n − k ) ! C(n, k) = \frac {n!}{k!\times(n-k)!} C(n,k)=k!×(n−k)!n!
利用已经定义的Factorial
函数,可以实现Combinations
函数。
完整程序如下:
/*
* Calculate the number of ways to choose a subset of
* k objects from a set of n distinct objects.
*/
#include <stdio.h>
/* Function Prototypes */
int Factorial(int n);
int Combinations(int n, int k);
/* Main Program */
main() {
int n, k;
printf("Input number of objects in the set (n): ");
scanf("%d", &n);
printf("Input number to be chosen (k): ");
scanf("%d", &k);
printf("C(%d, %d) = %d\n", n, k, Combinations(n, k));
}
/*
* Function: Factorial
* Usage: f = Factorial(n);
* ------------------------
* Calculate n!
*/
int Factorial(int n)
{
int product;
product = 1;
for (n=n; n > 1; n--) {
product *= n;
}
return product;
}
/*
* Function: Combinations
* Usage: ways = Combinations(n, k);
* ---------------------------------
* Calculate C(n, k)
*/
int Combinations(int n, int k)
{
return Factorial(n) / (Factorial(k) * Factorial(n - k));
}
combine.c
程序的一个运行示例如下:
当程序运行时,函数main
建立一个帧,它声明了两个变量:n
和k
。
当用户输入两个值后,程序到达了printf
语句,这时帧中的变量值,如下图所示:
为了执行printf
语句,计算机必须先算出调用combinations
函数得到的值,这将创建一个覆盖在原有帧上的一个新帧:
这个例子比上文的阶乘示例有更多的函数调用,每个新帧必须精确地记录在它做出调用前程序进行了哪些操作。
例如,本例在main
函数的最后一行出现了调用Combinations
,如下面程序中标记
M
1
M_1
M1所示:
当计算机执行一个新的函数调用时,它记录了一旦调用完成后调用程序应该从哪里继续执行。
执行的继续点被称为返回地址(return address),在这些图中用一个圆形标记指出。
为了记住程序执行的位置,应该在帧图上记录调用点,如:
一旦创建新的帧, 程序开始执行combinations
函数的函数体,它将用标记记录每个Factorial
函数的调用:
为了执行这个语句,计算机必须做出三个调用函数Factorial
的标记。
按照ANSI C制定的标准,编译器可以以任何次序做出这些调用。
假设选择按从左到右的顺序计算,第一个调用请求计算机计算Factorial(n)
,这将创建如下的新帧:
Factorial
函数按前面已介绍过的过程运行,并返回值120到标记
C
1
C_1
C1指出的调用点。
Factorial
函数的帧就消失了,回到Combinations
的帧中,继续下一步计算。
可以用返回并将调用结果填在调用的位置的方法来说明当前的状态,如下所示:
图中120外面的方框指出这个值不是程序的一个部分,而是一些以前计算的结果。
从这一点开始,计算机继续计算第二个Factorial
调用,其中实际参数为k
。因为k
有值2,这个调用将产生下列帧:
Factorial
函数再一次执行它的操作,但没有进一步调用,该函数将值2!(即2)返回到点
C
2
C_2
C2。将这个值返回到表达式,该表达式记录了如下结果:
现在还需要做出一次调用,它从计算实际参数表达式n-k
开始。
对于Combinations
帧中的n
和k
的值,该实际参数表达式的值为3,它创建了另一个帧,见下图:
从这点开始,Factorial
计算3!的值,并将值6返回到调用程序的
C
3
C_3
C3位置,产生了下列结果:
现在,Combinations
函数获得了计算结果所需的所有值,这个结果值为10。
为了找到这个结果的用途,需要咨询Combinations
帧,它又重新出现在栈顶上,见下图:
这个帧指出,程序应该获取这个返回值,并用它替换main
函数中的
M
1
M_1
M1处的函数调用。
如果获取了值10,并用它置换printf
语句中的Combinations
调用,可以得到下列状态:
给定了这个结果之后,printf
可以产生以下输出行:
这是该程序的最后一个操作。
理解C语言中的函数调用机制:
当程序调用一个函数时,函数执行它自己的操作,然后程序从调用点继续执行。
如果函数返回一个结果,调用程序在以后的计算中可以自由应用此结果。
参考
《C语言的科学和艺术》 —— 第5章 函数