5.3 调用函数
函数声明后,在其他程序中即可对其进行调用了。一般来说,C++程序都是从主函数main()开始执行,当执行到函数调用语句时,就会转去执行调用函数,执行后仍然返回到主函数,直至程序结束。当调用一个函数时,整个调用过程分为三步进行:第一步是参数传递;第二步是函数体执行;第三步是返回,即返回到函数调用表达式的位置。
5.3.1 函数调用格式
在具体讲解调用函数的其他内容前,先简要了解一下函数调用的格式。一般来说,函数调用的形式为:
<函数名>(<实参表>)
注意
实参应该与函数定义中的形参表中的形参一一对应,即个数相等、次序一致且对应参数的数据类型相同或相容。每个实参是一个表达式,并且必须有确定的值。
例如,下面的函数调用,其实参都不同:
一般来说,常见的函数调用方式有下列两种:
●将函数调用作为一条表达式语句使用,只要求函数完成一定的操作,而不使用其返回值。若函数调用带有返回值,则这个值将会自动丢失。例如:
●对于具有返回值的函数来说,把函数调用语句看作语句的一部分,使用函数的返回值参与相应的运算或执行相应的操作。例如:
需要注意的是,在函数原型声明中的参数称为形式参数(形参),而在函数调用中的参数称为实际参数(实参),实参是实际调用函数时所给定的常量、变量或表达式,它必须有具体的值。在主调程序和被调用的函数之间数据的传递是通过参数表来实现的。
此外,在C++中有两种不同的函数:库函数和自定义函数。库函数是C++系统提供的标准函数,用户一般不必自己定义,需要时直接调用即可。在调用库函数时,一般在文件的开头通过#include宏命令引用库函数对应的原型声明头文件,自定义函数则是根据程序的需要由用户自行定义。
提示
在实际程序中,#include命令后一般使用<>符号来调用库函数,使用“”符号来调用自定义函数。
5.3.2 传值调用
函数参数传递机制问题,本质上是调用函数(过程)和被调用函数(过程)在调用发生时进行通信的方法问题。前面内容提到了,函数调用的第一步就是传递参数。参数传递称为“实虚结合”,即实参向形参传递信息,使形参具有确切的含义(即具有对应的存储空间和初值)。根据参数传递的方式,函数调用可分为两种不同的方式,一种是按值传递,另一种是地址传递或引用传递。本节将介绍传值调用的实现,下一节将对传地址调用做详细讲解。
传值调用即其参数是按值传递的方式进行的。在值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在堆栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。以按值传递方式进行参数传递的过程如下:
①计算出实参表达式的值,接着给对应的形参变量分配一个存储空间,该空间的大小等于该形参类型的长度。
②把已求出的实参表达式的值一一存入到为形参变量分配的存储空间中,成为形参变量的初值,供被调用函数执行时使用。
这种传递把实参表达式的值传送给对应的形参变量,故称这种传递方式为“按值传递”。这种方式被调用函数本身不对实参进行操作,也就是说,即使形参的值在函数中发生了变化,实参的值也完全不会受到影响,仍为调用前的值。
【范例5-4】函数的传值调用。该范例定义了一个交换两个数的函数swap,在主函数main()中采用传值调用该函数,读者可查看其输出,实现代码如代码清单5-4所示。
代码清单5-4
【运行结果】在Visual C++中执行上述程序,其结果如图5-7所示。
图5-7 传值调用
【范例解析】上述代码首先输出未调用swap函数前x、y的值,在调用swap函数后,再一次输出x、y的值,看其是否达到了交换的功能。由图5-7的运行结果读者可以看出,x、y这两个值并没有交换,也即没有达到预期的目的,这就是传值调用。
简单来说,传值调用就是指当一个函数被调用时,C++根据实参和形参的对应关系将实参的值一一复制给形参,即实参的值单向传递给形参。函数本身不对实参进行任何操作,即使形参的值在函数中改变,实参的值也不会受到影响。
提示
为使程序可靠和便于调试,在程序中一般不改变实参的值,这时可采用按值传递的方式。
5.3.3 引用调用
函数的传值调用方式虽然容易理解,但形参值的改变不能对实参产生影响。因此传值调用方式在许多地方不适合用,如上述的两个数之间的交换函数,就无法用传值调用实现。此处就需要以引用作为参数,既能完成传值调用的功能,又使函数调用显得方便自然。
引用传递过程中,被调函数的形式参数虽然也作为局部变量在堆栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过堆栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
引用传递方式是在函数定义时在形参前面加上引用运算符“&”。在函数被调用时,参数传递的内容不是实参的值,而是实参的地址,即将实参的地址放到C++为形参分配的内存空间中,因此对形参的任何操作都会改变相应实参的值。
【范例5-5】函数的引用调用。要实现上述示例的在主函数main()中调用交换函数swap,使得两个数之间完成交换,就可以使用引用调用来实现,代码如代码清单5-5所示。
代码清单5-5
【运行结果】上述代码的执行结果如图5-8所示。
图5-8 引用调用
【范例解析】读者应该注意到了,代码5-5与代码5-4的区别就在于定义函数swap时,其参数前加上了&符号,其余代码均一致。增加了&符号即表示该函数被调用时采用的是引用调用,传递给函数的是实参的地址,因此,能够实现交换的功能。
提示
由于传递的是地址,在调用函数时不创建新的参数变量(开辟新的内存空间),因此在程序中对于占有内存较多的数据参数,为了节省内存,可采用引用传递的方式。
此外,引用传递还可以借助于指针(指针的概念将在后续章节中介绍),即在定义函数时,将形参说明成指针,而调用函数时就需要指定地址值形式的实参,这种参数传递方式也称地址传递,此处暂不做介绍。
5.3.4 嵌套调用
在前面章节介绍了,C++函数不能嵌套定义,即一个函数不能在另一个函数体中进行定义。但在使用时,允许函数的嵌套调用,即在调用一个函数的过程中又调用另一个函数,并且函数嵌套调用的层次可是多层的。例如,下面定义了一个函数func1,而在func2的函数体中调用了func2,这就是函数的嵌套定义,代码如下所示。
注意
此处需要注意的是,func1和func2是分别独立定义的函数,互不从属。并且在func1中调用func2函数前,func2函数已经被声明。
5.3.5 递归调用
在函数的调用中,还有一个较为特殊的情况。比如一个函数直接或间接地调用自身,这种现象就是函数的递归调用。递归调用有两种方式:直接递归调用和间接递归调用。直接递归调用即在一个函数中调用自身,间接递归调用即在一个函数中调用了其他函数,而在该其他函数中又调用了本函数。
递归调用的执行包括两个步骤:递推和回归。利用函数的递归调用,可将一个复杂问题分解为一个相对简单且可直接求解的子问题(“递推”阶段);然后将这个子问题的结果逐层进行回代求值,最终求得原来复杂问题的解(“回归”阶段)。
【范例5-6】函数的递归调用。该范例求出了整数n的阶乘,其采用的就是函数的递归调用,实现代码如代码清单5-6所示。
代码清单5-6
【运行结果】在Visual C++中输入上述代码,该程序的运行结果如图5-9所示。
图5-9 递归调用
【范例解析】读者可以看出,在定义函数Fac()的函数体中,其调用了本身来完成计算任务,这就是函数的递归调用。当n=5时,其执行流程为:先递推,其执行流程图如图5-10所示。
图5-10 递推流程
提示
递归就是先完成函数的递推,再进行回归。当图5-10递推到Fac(1)时,递推完成,开始回归,如图5-11所示。
图5-11 回归流程
简单来说,该程序的执行顺序如图5-12所示。
图5-12 递归执行顺序
使用函数的递归调用时,读者需要注意以下三个方面。
●递归算法设计简单,但消耗的上机时间和占据的内存空间比非递归大。
●设计一个正确的递归过程或函数过程必须具备两点:具备递归条件;具备递归结束条件。
●一般而言,递归函数过程对于计算阶乘、级数、指数运算有特殊效果。
5.3.6 带默认形参值的函数
在C++语言中调用函数时,通常要为函数的每个形参给定对应的实参。若没有给出实参,则按指定的默认值进行工作。当一个函数既有定义又有声明时,形参的默认值必须在声明中指定,而不能在定义中指定。只有当函数没有声明时,才可以在函数定义中指定形参的默认值。此外,默认值的定义必须遵守从右到左的顺序,如果某个形参没有默认值,则它左边的参数就不能有默认值。例如:
在进行函数调用时,实参与形参按从左到右的顺序进行匹配,当实参的数目少于形参时,如果对应位置形参又没有设定默认值,就会产生编译错误;如果设定了默认值,编译器将为那些没有对应实参的形参取默认值。
警告
形参的默认值可以为全局常量、全局变量、表达式、函数调用,但不能为局部变量。例如,下面的程序是不合法的。