2.8 函数
函数是对处理问题过程的一种抽象,通常在编程中将功能独立且经常被使用的某种功能抽象为函数。在C++语言中,函数同样重要,它是面向对象程序设计中对于某种功能的抽象,对于代码重用和提高程序的可靠性是非常重要的。
2.8.1 函数的定义
函数可以理解为实现某种功能的代码块,这样当程序中需要这个功能时就可以直接调用,而不必每次都编写一次,就好比生活中使用计算器来计算,当需要计算时,直接使用计算器输入要计算的数,计算完成后生成计算结果,而不必每次计算都通过手写演算出结果。在程序中,如果想多次输出“拼搏到无能为力,坚持到感动自己!”,就可以将这个功能写成函数,具体示例如下:
void表示该函数没有返回值,Output是为函数取的名字,Output后面有一对小括号,小括号中代表函数的参数,假如没有参数,小括号内为空。函数的主体从左大括号开始,到右大括号结束,中间是函数的功能。
当需要使用该函数时,就可以在程序中写入下面语句:
Output();
这就是调用函数,程序执行到这里,就会立即跳转到Output()函数的定义部分去执行(定义部分就是实现函数功能的部分),当函数执行完毕后,再跳回到原始位置继续往下执行。
从上述示例中,可以得出函数的定义,其语法格式如下:
C++函数的定义包括函数名、参数、返回类型和函数体,其中函数名、参数列表和返回值类型一起组成函数头,它是函数的接口,即调用这个函数时需要知道这些信息,而函数体是函数真正的实现。
在C++中,如果函数的定义在调用之前,则可以直接调用,但如果函数的定义出现在调用之后,则要先进行函数声明。为了提高程序的可读性,一般程序需要函数的声明。在C++中,函数声明包括函数返回类型、函数名和完整的形式参数列表。
函数名是一个标识符,它的命名规则与变量相同。在给函数命名时,应尽量使名字能够代表函数所完成的功能,这样可以增强程序的可读性。
函数参数是函数完成功能所需要输入的信息,如定义一个求两个整数和的函数,那么这两个数就要作为参数。一个函数可以有零个或多个任意数据类型的参数,参数之间用“,”隔开,参数名也是标识符。例如,下面语句是求两个整数和的函数定义,其中a与b就是两个参数,具体示例如下:
在上述函数定义中,参数a、b的值是从调用函数的地方传递过来的,在定义函数时还没有具体的值,因此称为形式参数,简称形参。与形式参数对应的是实际参数,即在调用这个函数时要传递给形式参数的具体量,简称实参。例如,下面的程序调用了add函数,其中i与j就是实际参数,具体示例如下:
在调用函数时,实参将自己的值传递给形参。在上面的程序中,i将自己的值1传递给对应的形参a,j将自己的值2传递给对应的形参b,这样a的值为1,b的值为2,如图2.37所示。
图2.37 实参与形参
函数的返回类型是函数在调用结束后返回值的数据类型,可以是除数组以外的任意类型。函数体中的return语句用来返回函数的结果,这个语句指示系统结束当前函数的执行,返回到调用这个函数的地方继续执行。如果定义的函数返回类型是void,则表示函数没有返回值,具体示例如下:
return;
此处也可以不写return语句,函数在执行到函数语句末尾的“}”自动结束调用返回。
当函数有返回值时,可以用下面的任意一种格式。具体示例如下:
return 表达式; return (表达式);
上述语句中表达式的值就是函数需要返回的值。
函数体是由一些语句组成的,这些语句共同完成了函数的功能。函数体中的语句可以是任意形式的语句,包括常量和变量的定义语句、表达式语句和流程控制语句等。如果变量的定义在函数体内,则这个变量称为局部变量,只能在这个函数体中使用;如果变量的定义在函数体外,则这个变量称为全局变量,可以在所有函数中使用。
接下来通过一个案例来演示函数声明、实现及调用,如例2-20所示。
例2-20
运行结果如图2.38所示。
图2.38 例2-20运行结果
在例2-20中,主函数中定义了两个整型变量,并从键盘读入这两个变量的值,然后调用函数add求这两个数的和并输出,注意区分实参a、b与形参a、b。
上例中函数的调用过程可以分为以下4步:
- 当函数调用开始时,建立调用函数的栈空间,保存调用函数的运行状态和返回地址,先将函数进栈,并为函数的形式参数按其数据类型分配动态内存。
- 将实参的值对应传递给形参。
- 执行函数体。
- 当执行到return语句或函数结束的“}”时,系统为返回值按返回值类型分配临时单元,并将返回值放入该单元,函数出栈,清理函数所占内存,返回值的临时单元参与主调函数中的所在表达式运算后销毁,继续主调函数的执行。
2.8.2 函数的参数传递
1. 普通型形式参数
函数形参与实参均为普通变量,函数调用时将实参的值复制一份给形参,在被调函数中,对形参的任何操作都不会影响实参的值。接下来演示普通型形式参数作为函数参数,如例2-21所示。
例2-21
运行结果如图2.39所示。
图2.39 例2-21运行结果
在例2-21中,从运行结果可发现,a、b的值并没有交换。这是因为,调用函数swap时,实参a、b的值会复制一份给形参a、b,在执行swap函数时,交换的是形参a、b的值,swap函数执行结束,形参a、b释放内存空间,这期间并没有改变实参中a、b的值,因此,打印结果中a、b值并不发生变化。
2. 指针型形式参数
当函数的形参是指针时,指针的值是一个地址,因而可以通过指针来间接访问该地址对应的内存空间。这种指针型形式参数提供了一种可以间接修改调用该函数的参数值的方法,但这种方法因其容易出错,所以很少使用,读者通过下面例题有所了解即可,如例2-22所示。
例2-22
运行结果如图2.40所示。
图2.40 例2-22运行结果
在例2-22中,从运行结果可发现,a、b的值交换了。这是因为,调用函数swap时,实参a、b的地址会复制一份给形参指针变量a、b,在执行swap函数时,通过∗访问指针变量a、b指向的内存空间,即实参a、b的值,这样就实现了交换a、b的值。
3. 数组型形式参数
在C++中,当形参被定义为数组时,数组参数自动转换为指针参数,因此调用函数时实际上是将实参(也是一个数组)的首地址传递给形参(一个指针变量),如例2-23所示。
例2-23
运行结果如图2.41所示。
图2.41 例2-23运行结果
在例2-23中,第3~17行代码为冒泡排序,具体过程如图2.42所示。数组参数实际上是一个指针,这个指针指向实参数组的首元素,因此可以通过指针来间接修改实参数组元素的值,也就是说,在函数中对数组参数所做的改变会影响到实参。
图2.42 冒泡排序过程
2.8.3 函数与引用
当指针作为函数参数时,形参的改变可以影响到实参,但在函数中反复使用指针,容易发生错误且难以理解。如果以引用作为函数形参,则既可以实现指针所带来的功能,而且更加高效。使用引用作函数形参时只需在函数定义时将形参前加上引用运算符“&”即可,如例2-24所示。
例2-24
运行结果如图2.43所示。
图2.43 例2-24运行结果
在例2-24中,swap()函数中的&a和&b就是引用作为函数形参,在执行swap(a,b)时,虽然看起来像是简单的变量传递,但实际上由于形参被声明成是实参的内存空间的引用,函数中对形参a、b的操作就是对所引用的实参a、b的内存空间的操作。
一个函数也可以返回为引用类型。返回引用的函数可以使函数出现在等号的左边,但是要求函数必须返回全局变量,如例2-25所示。
例2-25
运行结果如图2.44所示。
图2.44 例2-25运行结果
在例2-25中,函数CalArea用来计算一个圆的面积,形参r用来指定圆的半径。由于函数名前有运算符“&”,表示函数返回一个引用,因此该函数中return后面必须是一个已分配的内存空间的标识,不能是表达式。由于函数调用后,函数中的局部变量的内存空间被释放,因而函数不能返回一个局部变量的内存空间的引用。第12行变量a2引用函数CalArea返回的内存空间的值,因此area的值改变后,a2的值也会随之改变。第14行函数作为左值并进行赋值,此时area、a2的值也发生变化。
2.8.4 函数与const
const是不变的意思,这个关键字经常出现在函数的定义中,根据其出现在函数不同的位置,大致可以分为3类:修饰函数参数、修饰函数返回值和修饰类的成员函数。本节先讲解前两类,后一类在以后的章节中再讲解。
const修饰函数参数表示函数体中不能修改参数的值(参数本身的值或参数其中包含的值),具体示例如下:
函数返回值为const的情形只用在函数返回为引用的时候。当把返回为引用的函数再用const限定后,就表示这个函数不能作为左值使用,即不能被赋值。如例2-25中的CalArea()函数,如果定义成如下形式:
const double&CalArea(double r);
此时,执行下面的语句,会发生错误。
CalArea(5.0)=6.0f;
2.8.5 内联函数
在编写程序时,经常会遇到短小且使用频繁的代码,这时把这些代码写成函数,由于函数调用时,额外开销非常大,会降低程序的运行效率;但不写成函数,每次重复写相同的代码,程序的可读性降低。这种情况在C语言中通过宏函数来解决,由于宏只是简单的替换,不会进行类型检查等工作,很可能带来一些潜在的错误。在C++中通过内联函数可以解决这个问题,内联函数在实现过程上与宏函数相似,在编译时用函数体代替函数调用,节省执行时间。定义内联函数的方法很简单,即在函数头前面加上关键字inline,其语法格式如下:
例如,定义一个求两个整数和的函数为内联函数,具体示例如下:
其中,add函数是内联函数。在程序中出现的该函数的调用函数将用该函数的函数体代替,而不是转去调用该函数,因此内联函数可以提高运行效率。
内联函数的定义是有限制的,并不是所有的函数都可以定义成内联的,C++对内联函数的限制如下:
- 在内联函数中不能定义任何静态变量。
- 内联函数中不能有复杂的流程控制语句,如循环语句、switch语句、goto语句等。
- 内联函数不能递归。
- 内联函数中不能声明数组。
如果定义的内联函数比较复杂,违反了上述要求,那么即使使用inline限定,系统也将自动忽略inline关键字,把它当作普通函数处理。
2.8.6 默认参数的函数
C++是对C语言的改进,一方面是使得编译器能检查出更多的错误,另一方面减少编码的复杂程度。基于这两个方面,C++的函数中引入了默认参数的函数概念,即在定义或声明函数时给形参一个默认值,在调用函数时,如果不传递实参就使用默认参数值,如例2-26所示。
例2-26
运行结果如图2.45所示。
图2.45 例2-26运行结果
在例2-26中,第7行调用函数时,没有传递实参,形参a、b就使用默认值,最终函数返回3。第8行调用函数时,传递3给形参a,形参b使用默认值2,最终函数返回5。第9行调用函数时,传递5给形参a,传递6给形参b,此时没有使用默认值,最终函数返回11。
在使用默认参数时,需要注意以下几点。
- 默认值的指定只可在函数声明中出现一次。如果函数没有声明,则只能在函数定义中指定。
- 指定默认参数的顺序是自右向左。如果一个参数指定了默认值,则其右边的参数一定也要指定默认值。
- 默认参数函数调用时,实参列表遵循从左向右依次匹配的原则。
- 默认值不可以是局部变量。
2.8.7 函数重载
在实际开发中,有时候需要实现几个功能类似的函数,只是有些细节不同。例如,求两个数的和,这两个数可以是int、double等类型,在C语言中,由于每个函数必须有唯一的函数名,因此需要有如下两个函数:
int addInt(int a,int b); double addDouble(double a,double b);
这两个函数功能是相同的,都是求两个数的和。由于不同的函数名,给使用者带来诸多不便。因此考虑是否可以用同一个名字代替这两个函数名,在调用时根据参数的不同确定调用哪个函数,这便是C++提供的函数重载机制,上述两个函数可以使用同一个名字add,具体示例如下:
int add(int a,int b); double add(double a,double b);
上述两个函数就构成了函数重载,每个函数对应着不同的实现,即各自有自己的函数体。读者可能会疑惑在调用函数时编译器如何选择这些函数,具体原则如下:
(1)编译器根据重载函数的形式参数类型或参数个数的不同进行选择,因此构成重载函数必须在形式参数类型和参数个数上至少有一处不相同。例如,有3个同名函数,具体示例如下:
double fun(int a,double b); double fun(double a,int b); int fun(double a,int b);
上述代码中,第一个函数与第二个函数构成函数重载,因为函数的参数类型不相同。而第二个函数与第三个函数仅仅是函数的返回类型不同,因此不能构成函数重载。
(2)编译器选择重载函数是按一定的顺序将实参类型与所有被调用的重载函数的形参类型一一比较进行匹配,具体按如下顺序匹配:首先选择严格匹配的函数,再选择通过自动类型转换匹配的函数,最后选择通过强制类型转换匹配的函数。
接下来演示函数重载的用法,如例2-27所示。
例2-27
运行结果如图2.46所示。
图2.46 例2-27运行结果
在例2-27中,主函数中4次调用add函数,当传入不同的参数时调用对应的函数,在这个过程中,编译器会根据传入的参数与重载函数按照上面的顺序进行匹配,然后根据匹配结果调用不同的函数。
在使用具有默认参数的函数重载时需要注意调用时可能会发生歧义,具体示例如下:
void func(int a); void func(int a,double b=0);
当函数调用语句为“func(5);”时,它既可以调用第一个函数,也可以调用第二个函数,编译器无法确定调用哪一个函数,即发生了歧义,因此对函数进行重载时应避免设置默认参数。