第5章函数 引言 在程序设计过程中,如果程序的功能比较多,规模比较大,把所有代码都写在主函数中, 会使得主函数变得庞杂、逻辑不清,阅读和维护变得困难。在程序中,如果要多次实现某一 特定功能,需要多次重复编写实现此功能的程序代码,就会使得程序冗长、不精炼。解决的 办法就是采用模块化编程,即函数实现。函数是程序设计中具有独立功能的一段程序,是程 序的主要组成部分。C++程序由一个主函数和若干其他函数构成,主函数可以调用其他函 数,其他函数也可以互相调用。 学习目标 .了解:函数的作用;内存模型。 .熟悉:函数的定义、声明与调用;函数的嵌套调用与递归调用;作用域和存储类别。 .掌握:函数的编制方法;函数重载、内联函数和带默认值函数的特点。 课程思政 团队教育:通过函数模块化编程思想引入团队教育,讲解团队的重要性以及如何组建、 分工与协作,培养学生的团队精神和协作能力。 5.函数的定义与调用 1 5.1 函数概述 函数的定1. 义与声明 模块化程序设计就是把一个复杂问题分解成多个模块,每个模块独立完成一定的功能, 主程序按照问题求解算法调用这些模块。模块可以通过函数实现,这样主程序更加简洁,逻 辑思路更加清晰。C++是模块化编程语言。在面向过程程序设计中,模块就是函数;在面 向对象程序设计中,模块就是类,类里面再包括成员函数。函数的好处是,用户无须知道被 调用函数内部是如何实现的,直接调用函数实现相应功能,从而简化了用户的编程。 当启动一个C++程序时,从main() 函数开始执行。main() 函数可以调用其他函数,被 调用函数执行完成后再返回main() 函数,最后由main() 函数返回以结束程序。一般情况 下main() 函数不允许被其他函数调用。一个C+ + 程序可由多个源文件组成,但有且仅有 一个main() 函数。 1. 系统函数和用户自定义函数 从函数定义的角度看,可分为系统函数和用户自定义函数两类。 (1)系统函数是C+ + 编译系统、操作系统或其他系统为方便用户编程而预定义的函 数。这些函数在特定的头文件中都有原型说明。例如,am 头文件包含了一组处理输 iostre 95 入输出的对象和函数;cmath包含了一组数学计算的函数,如求绝对值的函数abs()、求平方 根的函数sqrt()。用户只需在程序前包含这些头文件,就可以在下面的代码中直接调用这 些函数。 (2)用户自定义函数是用户根据问题求解的需要自己编写的函数。这类函数往往功能 独特,使用范围比较有限,例如例5.1中的SumAbs()、PrintStars()函数。自定义函数是程 序设计最常见的方法,算法实现主要就是函数设计。 2.有返回值的函数和无返回值的函数 根据函数返回的结果,可分为有返回值和无返回值两类。 (1)有返回值的函数被调用执行完后将向调用者返回一个执行结果,称为函数返回值。 数学函数即属于此类函数,如求绝对值的函数abs()。由用户定义的有返回值的函数,必须 在函数定义和函数声明中明确返回值的类型。 (2)无返回值的函数用于完成某项特定的处理任务,执行完成后不向调用者返回函数 值。这类函数类似于其他语言的过程。由于函数无须返回函数值,用户在定义此类函数时 可指定它的返回为空类型,空类型的说明符为void。例如,PrintStars()函数实现打印星号 功能,无返回值。 3.无参函数和有参函数 从主调函数和被调函数之间数据传送的角度看,又可分为无参函数和有参函数。 (1)无参函数是指函数定义、函数声明及函数调用中均不带参数。主调函数和被调函 数之间不进行参数传送。此类函数通常用来完成一组指定的功能,可以返回或不返回函数 值,如PrintStars()函数。 (2)有参函数也称为带参函数。在函数定义及函数声明时都有参数,简称形参,称为形 式参数。在函数调用时也必须给出参数,称为实际参数,简称实参。进行函数调用时,主调 函数将把实参的值传送给形参,供被调函数使用,如SumAbs()函数。 【例5.1】 函数实例。 #include<iostream> using namespace std; #include<cmath> //包含若干数学函数的头文件 int SumAbs(int num1, int num2) //用户自定义函数,求两数绝对值之和 { int n1, n2, sum; n1 = abs(num1); //系统定义的函数,获取绝对值 n2 = abs(num2); sum = n1 + n2; return sum; }v oid PrintStars() //用户自定义函数,打印星号 { for (int i = 0; i < 10; i++) { cout << '*'; } cout << endl; 96 }i nt main() //主函数 { int num1, num2; PrintStars(); //函数调用,打印星号 cout << "请输入任意两个数:"; cin >> num1 >> num2; cout << "两数的绝对值之和为:" << SumAbs(num1, num2) << endl; //函数调用,求两数绝对值之和 PrintStars(); //函数调用,打印星号 } 程序执行结果如下: ********** 请输入任意两个数:-10 20 两数的绝对值之和为:30 ********** 在本例中,abs是系统定义的绝对值函数,可以直接调用,但是需要使用语句#include <cmath>将相应的头文件包含进来。用户自定义了有参数和返回值的SumAbs函数,实 现求两个数的绝对值之和,定义了无参数和返回值的PrintStars函数,打印一行星号。 5.1.2 函数的定义 在C++程序中用到的函数必须先定义后使用。函数定义的语法格式如下: 函数类型 函数名(形参表) { 函数体 } 一个函数定义由两部分组成:函数头和函数体。函数头包括函数类型(即返回值类 型)、函数名以及形参表,确定了该函数的语义功能。函数体为函数提供一种实现方式,用一 对花括号括起来,由一组语句组成,确定了该函数执行时的具体操作。 【例5.2】 用户自定义函数Max和Welcome,分别实现求两个数中的较大数和显示欢 迎信息的功能。 #include<iostream> using namespace std; int Max(int num1, int num2) //有参数和返回值的函数,求两个整数中的较大数 { return num1 > num2 ? num1 : num2; }v oid Welcome() //无参数和返回值的函数,显示欢迎信息 { string name; cout << "请输入您的名字:"; cin >> name; cout << name << ",欢迎您!"; 97 return; }i nt main() //主函数 { int num1, num2; cout << "请输入任意两个数:"; cin >> num1 >> num2; cout << "两个数中的较大数为:" << Max(num1, num2) << endl; //函数调用,求较大数 Welcome(); //函数调用,显示欢迎信息 return 1; } 程序执行结果如下: 请输入任意两个数:10 20 两个数中的较大数为:20 请输入您的名字:阿强 阿强,欢迎您! 说明: (1)函数类型指该函数结束执行时要返回结果的数据类型。若不需要返回值可以写为 void。 (2)函数名是用户自定义的名称,要符合C++标识符的命名规则。 (3)形参表指调用该函数时向它传递的数据,分别对形式参数进行定义,参数之间用逗 号进行间隔。例5.2中的Max()函数定义了num1和num2两个形式参数。 (4)函数体指函数所要完成的功能。函数体包括变量声明部分和执行语句部分。 (5)函数定义中的一对花括号不能省略,它用于指明函数体的开始和结束。 (6)当函数有返回值时,如Max()函数,在函数体中至少应有一条return语句,函数返 回值就是return后面表达式的值。当函数没有返回值时,如Welcome()函数,可以写为 return;或者省略return语句。 5.1.3 函数的调用 一个函数被定义后,通过被其他函数调用实现该函数的功能。对于一个函数,只有被调 用时,其函数体才能执行。一个函数调用就是确定一个函数名并提供相应的实参,然后开始 执行其函数体中的语句。当函数体执行结束时,函数调用就得到了一个值,返回给调用方。 1.函数调用的形式 函数调用的形式如下: 函数名(实参表) 函数名是已定义的一个函数的名字;实参表由零个、一个或多个实参构成。如果是调用 无参函数,实参表为空,但圆括号不能省。在实参表中,每个实参都是一个表达式。实参表 中的实参与被调用函数的形参表中的形参要求在个数、类型、顺序上一致。在函数调用过程 中,先计算各实参的值,再赋给对应的形参,最后再启动执行函数体。 2.函数调用的方式 按函数调用在语句中的作用划分,有3种函数调用方式。 98 (1)语句调用。是指函数调用作为一条语句单独出现,通过加上一个分号构成一条语 句。其调用格式为 函数名(实参表); 例如,在例5.2中: Welcome(); Sum(10, 20); 都是函数调用语句。这种调用方式忽略了函数的返回值。对于无返回值的函数调用,就只 能用这种调用方式;对于有返回值的函数调用,也能如此调用,只是忽略了返回值。但是,如 果返回值比较重要,如此调用就失去了意义。 (2)表达式调用。是指函数调用作为一个表达式出现。其调用格式为 变量名=函数名(实参表); 或者 cout<<函数名(实参表); 函数调用本身就是一个表达式,也具有特定的返回值类型,因此函数调用可出现在表达 式中,以函数返回值参与表达式的计算。这种方式要求函数有返回值,而不能是void。 例如: max = Max(num1, num2); //把Max 函数的返回值赋予变量max cout<<Max(num1, num2); //输出Max 函数的返回值 (3)参数调用。是指一个函数调用作为另一个函数调用的实参。这种情况是把前者的 返回值作为后者的实参,因此要求前者必须是有返回值的。例如: max = Max(Max(num1, num2), num3); 把Max函数调用的返回值又作为Max函数的实参,求出num1、num2、num3的最大 值。此时将返回值作为实参的内层Max函数调用要先执行,返回的结果作为外层Max函 数调用的实参值,再次调用Max函数执行。 5.1.4 函数的声明 在编写较大规模的模块化程序时,因为程序中的函数定义较多,占用很多代码行,对整 个程序的可读性会产生一定的影响。为了避免这种情况,可对函数先声明,后给出定义。函 数声明又称函数原型说明,用来告诉编译器函数的名称、返回值类型以及函数要接收的参数 个数、类型和顺序,编译器用函数原型验证函数调用。 一个完整的函数定义包含一个函数头和一个函数体。其中函数头就是函数原型,包括 函数的名称、形参及返回值。函数原型说明的语法格式为 函数类型 函数名(参数表); 函数原型说明由函数类型、函数名和参数列表组成。参数表与函数定义中的参数表在参 数个数、顺序和类型上应一致。在函数原型说明中可以不给出参数名,只给出类型。例5.2的 程序可改写如下: 99 #include<iostream> using namespace std; int Max(int, int); //函数原型说明 void Welcome(); //函数原型说明 int main() { int num1, num2; cout << "请输入任意两个数:"; cin >> num1 >> num2; cout << "两个数中的较大数为:" << Max(num1, num2) << endl; //函数调用 Welcome(); //函数调用 return 1; }i nt Max(int num1, int num2) //函数的定义 { return num1 > num2 ? num1 : num2; }v oid Welcome() //函数的定义 { string name; cout << "请输入您的名字:"; cin >> name; cout << name << ",欢迎您!"; return; } 函数的定义是平行的,不能嵌套定义,但可以嵌套调用。主函数main和最大值函数 Max地位是平行的。主函数要调用Max函数,因此需要在主函数调用Max函数前,对Max 函数进行函数原型说明,原型说明用于对调用的函数与函数的定义进行对照检查。 一个函数只能定义一次,但函数原型说明可以在程序中多次出现。先说明函数原型,使 下面的代码能调用该函数,检查函数调用是否符合语法规则。函数的完整定义可以放在本 源程序的后面、另一个源程序中或者编译后的库文件中。在C++中,一般将main函数放在 最前面,因为它通常提供了程序的整体结构;而将函数的完整定义放在后面。这样,程序的 结构就更加清晰。 对于一组通用的函数,如果希望它能被更多的用户在更多的程序中调用,但又不能公开 源代码而被人篡改,也不想反复编译而浪费时间,可将这些函数原型说明放在一个头文件 (扩展名为.h)中,相应的函数定义放在其他源文件中,再将这些源文件编译为库文件,最后 将头文件和库文件公开给他人使用。就像使用cmath头文件中的abs函数一样,先用# include<cmath>说明函数原型,然后就能调用其中的abs函数了。 5.2 函数参数与函数返回 5.2.1 函数参数 在调用函数时,大多数情况下,主调函数和被调函数之间有数据传递关系,这就是前面 提到的有参数的函数形式。函数参数的作用是传递数据给函数,使函数利用接收的数据进 100 行相应的操作。 1.形式参数与实际参数 在定义函数时,把函数括号中的变量名称为形式参数,简称形参。在函数调用时,传递 给函数的值将被复制到这些形参中。 在调用函数时,把函数括号中的参数称为实际参数,简称实参。实参是指在调用函数时 函数的调用者提供给函数形参的实际值。实参可以为常量、变量和表达式。 函数的形参和实参具有以下特点: (1)形参只有在被调用时才分配存储单元,在调用结束时即刻释放其占用的存储单元。 因此,形参只在函数内部有效,函数调用结束返回主调函数后则不能再使用该形参变量。 (2)实参可以是常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调 用时,它们都必须具有确定的值,以便把这些值传送给形参。因此应预先用赋值、输入等办 法使实参获得确定值。 (3)实参和形参在个数、类型和顺序上应严格一致,否则会发生类型不匹配的错误。 (4)函数调用中发生的数据传递是单向的。即只能把实参的值传递给形参,而不能把 形参的值反向地传递给实参。因此,在函数调用过程中,形参的值发生改变,而实参的值不 会改变。 【例5.3】 求两个整数的和。 #include<iostream> using namespace std; int Sum(int, int); //函数原型说明 int main() { int n1, n2; cout << "请输入任意两个整数:"; cin >> n1 >> n2; cout << Sum(n1, n2); //函数调用,n1 和n2 为实参 cout << Sum(10, 2*10); //函数调用,10 和2*10 为实参 }i nt Sum(int num1, int num2) //函数的定义,num1 和num2 为形参 { return num1 + num2; } 程序执行结果如下: 请输入任意两个数:10 20 两个数中的较大数为:20 请输入您的名字:阿强 阿强,欢迎您! 在定义函数时,num1和num2为形参。在调用函数时,变量n1和n2、常数10和表达 式2*10是实参。函数调用时,实参的值被复制给形参,在函数体内进行运算。 在C++中,函数调用中要求形参和实参要一一对应。在函数调用时,系统为形参分配 存储空间,并将实参的值传递给形参。在函数调用过程中,参数的传递方式有3种:值传 递、地址传递和引用传递。下面介绍值传递和地址传递,引用传递将在6.5.2节详细讲解。 101 2.值传递 值传递是参数传递数据最常用的方式。值传递的特点是单向传递,即主调函数调用被调 函数时给形参分配存储单元,把实参的值传递给形参;在调用结束后,形参的存储单元被释放, 而形参值的任何变化都不会影响到实参的值,实参的存储单元仍保留并维持其值不变。 【例5.4】 编写Swap函数,实现两个整数的互换。 #include<iostream> using namespace std; void Swap(int, int); int main() { int n1 = 10; int n2 = 20; cout << "交换前,main 函数中:n1=" << n1 << " n2=" << n2 << endl; Swap(n1, n2); //值传递,实参复制一个副本给形参 //形参值的改变不会影响对应实参的值 cout << "交换后,main 函数中:n1=" << n1 << " n2=" << n2 << endl; return 1; }v oid Swap(int num1, int num2) { cout << "Swap 函数中,交换前:num1=" << num1 << " num2=" << num2 << endl; int temp = num1; num1 = num2; num2 = temp; //形参值发生了改变 cout << "Swap 函数中,交换后:num1=" << num1 << " num2=" << num2 << endl; } 程序执行结果如下: 交换前,main 函数中:n1=10 n2=20 Swap 函数中,交换前:num1=10 num2=20 Swap 函数中,交换后:num1=20 num2=10 交换后,main 函数中:n1=10 n2=20 main函数调用Swap函数时,实参n1、n2的值传递给形参num1、num2。在Swap函数 中,形参num1和num2的值进行交换,但main函数中实参n1和n2的值并没有改变。 在本例中,Swap函数没有真正实现两数的互换。如果要实现这个功能,就需要使用下 面的地址传递这种方式。 3.地址传递 地址传递是指形参接收到的是实参的地址,即指向实参的存储单元,形参和实参占用相 同的存储单元,因此形参和实参是相同的。在以数组名或指针作为函数参数时进行的传递 就是地址传递。形参在取得该地址之后,与实参共同拥有一段内存空间,形参的变化也就是 实参的变化。将例5.4的值传递改为地址传递,就可以真正地实现两数的互换。 【例5.5】 编写Swap函数,通过地址传递实现两个整数的互换。 #include<iostream> using namespace std; 102 void Swap(int*, int*); int main() { int n1 = 10; int n2 = 20; cout << "交换前,main 函数中:n1=" << n1 << " n2=" << n2 << endl; Swap(&n1, &n2); //地址传递,实参将自己的地址传递给形参 cout << "交换后,main 函数中:n1=" << n1 << " n2=" << n2 << endl; return 1; }v oid Swap(int* num1, int* num2) //形参定义为指针,接收实参传递的地址 { int temp = *num1; *num1 = *num2; *num2 = temp; } 程序执行结果如下: 交换前,main 函数中:n1=10 n2=20 交换后,main 函数中:n1=20 n2=10 num1、num2被定义为指针变量(指针在第6章详细讲解),main函数调用Swap函数 时,将实参n1、n2的地址传递给形参。这样,形参num1和num2分别指向实参n1和n2,从 而就可以改变n1和n2的值。 5.2.2 函数返回 主调函数和被调函数具有严格的控制与被控制的关系。当主调函数调用被调函数后, 主调函数处于等待状态,转而执行被调函数;当被调函行结束后,必须返回主调函数,主调函 数才能从等待处继续执行。 被调函数通过return语句返回主调函数,其功能包括:①结束被调函数的执行,返回主 调函数;②在结束被调函数时,可以将返回值传递给主调函数。 1.有返回值的函数 如果函数定义时确定了返回值的类型,那么被调函数执行完后要向主调函数返回一个 结果,就是函数返回值。返回值的类型就是该函数的类型。函数的返回值只能通过return 语句返回主调函数。return语句的一般形式为 return 表达式; 或者 return (表达式); 【例5.6】 编写判断一个整数是否为素数的函数,实现输出100~200的素数,每行输出 8个数,最后输出这些素数的和。 #include<iostream> using namespace std; int IsPrime(int n); int main() 103 { int count = 0; //控制每行素数的输出数量 int sum = 0; //100~200 的素数和 cout << "100~200 的素数:" << endl; for (int i = 100; i <= 200; i++) if (IsPrime(i)) { sum = sum + i; cout << i << " "; if (++count % 8 == 0) cout << endl; } cout << endl; cout << "100~200 的素数之和为:" << sum; return 0; }i nt IsPrime(int n) //定义函数,判断一个整数是否为素数 { int i; for (i = 2; i < n; i++) if (n % i == 0) return 0; //不是素数,返回值为0 return 1; //是素数,返回值为1 } 程序执行结果如下: 100~200 的素数: 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 100~200 的素数之和为:3167 说明: (1)return语句将被调函数中的一个确定值带回主调函数中。 (2)若需要从被调函数带回一个值供主调函数使用,被调函数必须包含return语句。 (3)一个函数体中可以有一个以上的return语句,执行到哪一个return语句,哪一个 就起作用。return语句后面的括号可以不要。例如,return0;等价于return(0);。 (4)return后的值可以是一个表达式。 (5)在定义函数时,函数类型一般应和return语句中的表达式类型一致。如果两者不 一致,则以函数类型为准。对数值型数据可以自动进行类型转换。 2.无返回值的函数 如果函数定义时确定返回void,那么该函数的调用执行完成后将不会返回任何值。例 如,setw(6)是一个函数调用,它的执行不会返回值,所以只能进行单独语句调用,而不能直 接参与表达式计算。注意,无返回值并不意味着函数执行没有结果。一个函数的计算结果 可能作用在函数之外的数据上,而不一定要返回。