2.6.3 函数原型
现在回到程序开头,将注意力放在函数cusum的函数体,它的大部分内容都是我们在上一章里讲过的,变化不大。和从前一样,我们首先声明了两个变量n和sum,然后分别用表达式语句来修改它们的值。和变量r一样,变量n和sum也是在函数cusum开始执行时被创建和开辟出来的,在函数返回时销毁。当下一次再调用此函数时,又将创建和开辟新的变量r、n和sum。
这就是说,变量r、n和sum只存在于程序运行过程中的某一段时间。事实就是这样,有些变量的存在时间很长,甚至和整个程序的运行时间一样长,而有些变量的存在时间很短。不管多长多短,变量在程序运行时的存在时间称为变量的生存期。
和上一章相比,while语句有一点变化,那就是它的控制表达式变成了n<= r,而不是从前的n<= 100。所以,现在是用变量n的值和变量r的值做比较,这要先分别对子表达式n和r进行左值转换,再对转换后的值进行比较。
在函数体的末尾,return语句结束当前函数的执行,将控制返回到它的调用者。左值转换是普遍存在的,该语句在执行时,先计算表达式sum的值,也就是先进行左值转换,然后向调用者返回转换后的值。
就这样,函数cusum结束了它的一次执行。现在回到main函数,有三条语句都调用了cusum函数,除了传递的实际参数不同外,它们的执行过程其实都差不多。
程序的执行和函数声明的先后次序没有任何关系,在程序中,虽然说cusum函数声明在前而main函数的声明在后,但是,程序启动后将首先调用main函数。
然而,虽然程序的执行顺序和函数的相对位置无关,但却会影响函数调用本身。我们已经说过,变量必须先声明后使用,函数也是这样,函数的声明必须位于函数调用之前。函数的声明有两种形式,一种是带有函数体的函数声明,另一种是不带函数体的函数声明,带有函数体的函数声明称为函数定义。
在源文件c0202.c中,函数cusum的定义位于main函数之前,之后才在main函数内调用了它。但是,如下面的程序所示,如果我们将cusum函数的定义放在main函数之后,则必须在调用之前做一个不带函数体的声明。
/*****************c0203.c*****************/ unsigned long long int cusum(unsigned long long int); int main(void) { unsigned long long int x, y, z; x = cusum(10); y = cusum(100); z = cusum(1000); } unsigned long long int cusum(unsigned long long int r) { unsigned long long int n, sum; n = 1; sum = 0; while(n <= r)
{ sum = sum + n; n = n + 1; } return sum; }
在不带函数体的函数声明中,参数列表中的标识符(形参的名字)可以省略,而且该声明必须以分号“;”结束。因此,以下两种声明方式都是合法的:
unsigned long long int cusum(unsigned long long int r); unsigned long long int cusum(unsigned long long int);
在程序翻译期间,翻译器对函数调用作语法检查,看参数的数量和类型是否与函数的声明一致。如果不一致,将输出错误信息并中止翻译过程。
练习2.2
是否可以用表达式cusum( )和cusum(100, 20)来调用函数cusum?为什么?请上机验证。
在C语言诞生之初,函数的声明并不是这个样子的,用今天的眼光来看,着实十分古怪。以源文件c0203.c为例,要是用早期的方式来写,会是什么样子呢?
经过改写的源文件如下所示,但并不是完全“仿真”的,因为在那个时代,C语言还没有引入unsigned long long int类型。
/****************c0204.c****************/ unsigned long long int cusum(); //D1 main() //D2 { unsigned long long int x, y, z; x = cusum(10); y = cusum(100); z = cusum(1000, 1200); //S1 return 0; } unsigned long long int cusum(r) //D3 unsigned long long int r; //D4 { unsigned long long int n, sum;
n = 1; sum = 0; while(n <= r) { sum = sum + n; n = n + 1; } return sum; }
先来看main函数的声明,它从D2处开始。可以看出,函数main的声明中没有返回类型,这在当时是允许的,如果函数的返回类型是int的话,则它可以省略。另一个显著的特点是函数名右边只是一对圆括号,内容为空。在那个时代,C语言里还没有引入关键字“void”,如果函数没有参数,它只能这样写。事实上,即使函数有参数,不带函数体的函数声明也必须这样写。
从D3处开始的部分是函数cusum的声明,因为带有函数体,这也是它的定义。但是这种定义方式很奇怪,函数名右边的圆括号内只允许是参数的名字(标识符),对参数的声明(D4)夹在D3和函数体的左花括号“{”之间。
和现在一样,函数在调用前必须声明。如果函数的定义位于main函数之后,则它必须有一个不带函数体的声明。在程序中,函数cusum是在函数main之后定义的,为了在函数main内调用它,D1处是它的前置声明。按照以往的惯例,函数名右边的圆括号内应当为空。
由于D1处的声明并未指定参数信息,对此函数的调用就只能依靠程序员的自律了。如果他胡来,也没有办法阻止。如语句S1所示,即使我们知道这个函数只接受一个参数,我们也可以为它传递两个甚至多个参数。在翻译期间,翻译器连吭都不吭一声,因为它无法从D1处获得参数的数量和类型信息。
标准化之后的C语言借鉴了其他编程语言的经验,对函数的声明做了改进,其中最主要的一点是将参数的声明放在函数名后面的圆括号内,而且必须指定参数的类型;如果函数的声明不带函数体,可省略参数的名字,但必须指定其类型。现在,我们把带有参数类型声明的函数声明称为函数原型。
引入函数原型的原因是便于C实现在程序翻译期间实施类型检查。在调用函数时,如果参数的数量和类型与函数的声明不匹配,则它很容易被检查出来。
练习2.3
在变量的声明中,标识符是变量的名字,在表达式里,它是一个代表变量的( );在函数的声明中,标识符是函数的名字,在表达式里,它是一个代表函数的( );带函数体的函数声明被称为( )。
备选答案:(A)函数定义(B)函数指示符(C)左值