第七章函数
在前面的章节中,我们编写的程序都是由一个main函数组成,在main函数中调用了scanf、printf等系统函数。然而一个实际问题的编程不可能把所有要完成的任务全都写在main函数中。这时,程序往往由一个main函数和若干个其它函数组成,每个函数各自完成相对独立的部分功能。程序从main函数开始执行(不管其位置如何),在main函数结束(除遇exit外)。其它函数只在被调用时才能被执行。
被调用的函数可以是系统提供的库函数,也可以是用户自己定义的函数。图7.1是函数间相互调用的示意图。
main() f1() f11()
{ { {
┇┇┇
f1(); f11(); }
┇┇ f12()
f2(); f12(); {
┇┇┇
} } }
f2() f21()
{ {
┇┇
f21(); }
┇
}
图7.1 函数间相互调用的示意
7.1 库函数
库函数由系统提供,用户只要按照要求的格式正确调用即可。不同的C编译系统提供的库函数有所不同。
说明:
(1)调用库函数时要用#include命令将相关的头文件包含进来。
如:调用数学函数,用#include “math.h”或
调用输入输出函数,用#include “stdio.h”或
(2)库函数调用的一般形式:
函数名(参数表)
要注意函数的功能、参数的个数与类型、函数值的类型。
如求平方根的函数sqrt的形式是:double sqrt(double x)
即sqrt函数的参数只有一个,为双精度型,函数的返回值也是双精度型。在调用时要注意这些要求。如在判断数m是否为素数时,只需要判断该数是否能被2~m除尽即可。
用程序实现为:
int m,n,i;
……
n=(int)sqrt((double)m);
for(i=2;i<=n;i++)
……
即将整型变量m强制转换成double型后,调用sqrt函数,因函数的返回值也为double 型,再又将函数值强制换成int型后赋值给整型变量n,便于下面的处理。
(3)库函数调用以两种方式出现。
①出现在表达式中,即作为表达式的一部分参与运算。
如:计算y=x2.5+1.3,则通过以下语句调用pow函数来实现。
y=pow(x,2.5)+1.3;
②独立的语句,即调用函数后加一分号。
如:printf(“*****\n”);
(4)调用库函数时,要注意参数的一些特殊要求。如三角函数要求自变量参数用弧度表示,开平方函数要求自变量参数的值大于或等于0。
7.2 函数的定义和返回值
虽然C语言提供了丰富的库函数,但不可能满足每个用户的各种特殊需要,因此大量的函数必须由用户自己来定义(编写)。
7.2.1 函数的定义
所谓函数定义实际上就是在程序中写一个函数(模块)。
函数定义有如下两种形式:
形式一
函数返回值的类型名函数名(类型名形式参数1, /* 函数的首部 */
类型名形式参数2,……) /* 函数的首部 */ {
声明部分 /* 函数体 */
语句部分
}
形式二:
函数返回值的类型名函数名(形式参数1,形式参数2,……)/* 函数的首部 */ 形式参数类型说明 /* 函数的首部 */ {
声明部分 /* 函数体 */
语句部分
}
一般把形式一称为现代方式,而将形式二称为传统方式。Turbo C和目前使用的多数C 版本对这两种方式都允许采用,两种方式等价。ANSI新标准推荐使用形式一,即现代方式。
例如,求两个数中的较大者的程序为:
main()
{
int a,b,max;
scanf(“%d%d”,&a,&b);
max=max2(a,b);
printf(“max=%d\n”,max);
}
int max2(int x,int y)
{
int z;
if(x>y)
z=x;
else
z=y;
return(z);
}
其中:int max2(int x,int y) 也可以写成:
int max2(x,y)
int x,y;
说明:
(1)在定义时,若省略了函数返回值的类型名(函数值的类型),则默认为int类型。如上面程序中max2函数的首部可写为:
max2(int x,int y)
(2)函数名和形式参数(简称形参)均为由用户命名的标识符。同一程序中,函数名必须唯一;同一函数中,形参名也必须唯一,但可以与其它函数中的变量同名。
(3)各形参的定义之间用逗号隔开,可以没有形参(称为无参函数),但一对圆括号不能省。(4)除形参外,凡在函数中用到的变量必须在声明部分进行定义(放在语句部分之前)。(5)形参和函数体中定义的变量只在函数被调用时才临时分配存储单元,当退出函数时,这些存储单元全部被释放(称为局部性,因而与其它函数中的变量同名不会引起混淆)。(6)函数体可以为空,称为空函数。
如:dummy()
{
}
这是一个什么也不做的函数。在设计程序的初期,它仅代表这里有一个函数,而函数体的具体内容待以后再补上。
(7)函数的定义是平行的,不能在一个函数的内部再定义函数。即每个函数是一个相对独立的模块,不能在写某一个函数时又包含了另一个函数的定义。
7.2.2 函数的返回值
函数的值通过return语句返回。
return语句的形式:
return(表达式);
或 return 表达式;
表达式的值就是所求的函数值。
当执行到return语句时,程序流程返回到调用该函数的地方,并带回函数值。
说明:
(1)表达式的类型应与函数首部所说明的类型一致。若不一致,由系统自动转换为函数值的定义类型,即函数定义类型决定返回值类型。
如:main()
{
float a,b,max;
scanf(“%f%f”,&a,&b);
max=max2(a,b);
printf(“max=%f\n”,max);
}
int max2(float x,float y)
{
float z;
if(x>y)
z=x;
else
z=y;
return z;
}
则将表达式z的值(float型)自动转换为函数定义类型(int型)后带回到main函数中。
(2)若无表达式,则return语句的作用只是使流程返回到调用函数,无确定的函数值。(3)在一个函数内,可以有多个return语句,执行到哪一个return,就以此return后的表达式值作为函数值。注意,并非返回多个值。
main()
{
int a,b,max;
scanf(“%d%d”,&a,&b);
max=max2(a,b);
printf(“max=%d\n”,max);
}
int max2(int x,int y)
{
if(x>y)
return x;
else
return y;
}
尽管max2函数中出现过二个return语句,但只能返回x、y中的一个值,不能同时返回x、y的值。
(4)若函数体内无return语句,则执行到函数末尾的“}”后返回到调用函数,无确定的函数值返回。
(5)为了明确说明函数无返回值,可将函数的返回值类型定义为void,称为“无类型”或“空类型”。
如:void prm()
{
printf(“message\n”);
}
(6)在C语言中,函数名不能被赋值,这一点与有些语言是不同的。
7.3 函数的调用
7.3.1 函数调用的一般形式
所谓函数调用就是把控制流程转到被调用函数中去执行函数的各语句。
函数调用的一般形式为:
函数名(实在参数表)
实在参数(简称实参)可以是常量、变量或表达式,多于一个时,彼此间以逗号隔开。若函数定义时无形参,则调用时无实参,但一对圆括号不可少。
说明:
(1)调用函数时,其名字必须与定义的名字相同。
(2)函数调用时的实参不需要带实参类型。
如上面main函数中对max2函数的调用不能写成:
max=max2(int a,int b);
(3)实参与形参在个数、类型、次序上应保持一致。
(4)函数必须先定义,后调用。返回值类型为int或char的函数可例外。
(5)函数可直接或间接地自己调用自己(称为递归调用,在7.6节介绍)。
7.3.2 函数调用的方式
(1)作为表达式出现在允许表达式出现的任何地方。
如:c=max2(a,b);
或 printf(“%d\n”,max2(a,b));
(2)作为一条独立的语句。
形式为:函数名(实在参数表);
如:dummy();
printf(“a=%d,b=%d\n”,a,b);
(3)作为另一次函数调用的实参。
如:d=max2(max2(a,b),c);
7.4 函数的声明
在C语言中,除了主函数外,对于用户定义的函数要遵循“先定义,后使用”的规则。凡是未在调用之前定义的函数,C编译程序默认函数的返回值为int类型。对于返回值为其它类型的函数,若把函数的定义放在调用之后,应该在调用之前对函数进行声明(或称为函数原型声明,有的也称函数说明)。
7.4.1 函数声明的形式
函数声明的一般形式:
类型名函数名(参数类型1 参数名1,参数类型2,参数名2,……)或类型名函数名(参数类型1,参数类型2,……)
如:
main()
{
float a,b,c;
float max2(float x,float y); /* 函数的声明 */
scanf(“%f%f”,&a,&b);
c=max2(a,b); /* 函数的调用 */
printf(“%f\n”,c);
}
float max2(float x,float y) /* 以下为函数的定义 */
{
float z;
if(x>y)
z=x;
else
z=y;
return (z);
}
对函数进行声明,能使C语言的编译程序在编译时对函数的调用进行有效的类型检查。说明:
(1)函数声明中的形参名是一种虚设,它们可以是任意的用户标识符,既不必与函数首部中的形参名一致,又可以与程序中的任意用户标识符同名。因此,参数名可以省略,但参数的类型、个数和次序必须与函数定义的首部一致。
如上面的例子中函数声明可写成:float max2(float,float);
(2)函数定义与函数声明是不同的,定义是写出函数的完整形式,包括函数首部及函数体,是一个完整、独立的函数单位,而声明是告诉系统此函数的返回值类型、参数的个数与类型,即函数首部的信息。
(3)若函数的返回值类型为int或char,则可以不进行函数声明(系统默认)。但是使用这种方法时,系统无法对参数的类型做检查。若调用函数时参数使用不当,在编译时也不会报
错。因此,为了程序清晰和安全,建议都进行声明为好。
(4)若定义在前,调用在后,则可不进行函数声明。因为编译系统已经先知道了已定义的函数类型,会根据函数首部提供的信息对函数的调用作正确性检查。
如上面的例子可以写成:
float max2(float x,float y) /* 以下为函数的定义 */
{
float z;
if(x>y)
z=x;
else
z=y;
return (z);
}
main()
{
float a,b,c;
scanf(“%f%f”,&a,&b);
c=max2(a,b); /* 函数的调用 */
printf(“%f\n”,c);
}
7.4.2 函数声明的位置
(1)放在调用函数的声明部分(只有此调用函数能识别被调用函数)。可以是独立语句,也可与其它变量的定义放在同一个语句中。
如上面的用法还可写成:
float a,b,c,max2(float,float);
(2)放在所有函数的外部,被调用之前。(此时函数声明的后面所有位置上都可对该函数进行调用)。
如:float max2(float,float);
main()
{
float a,b,c;
scanf(“%f%f”,&a,&b);
c=max2(a,b);
printf(“%f\n”,c);
}
float max2(float x,float y)
{
……
}
(3)调用库函数时,要在程序的开头使用#include来包含相关的头文件,就是因为头文件
中包含了这些库函数的原型声明。
7.5 调用函数和被调用函数之间的数据传递
7.5.1 变量作为参数
当变量作为形参时,对应的实参可以是常量、已有值的变量(或数组元素)或有确定值的表达式。当调用函数时,把实参的值传递给形参,形参得到值后,在被调函数中进行运算,若有return语句,则通过return语句把函数值返回到调用函数。
说明:
(1) 形参在接收实参传过来的值时,在内存临时开辟新的存储空间,以存放形参的值,当函数执行完毕时,这些临时开辟的内存空间会被释放,并且形参的值在函数中不论是否发生变化,都不会影响实参的值的变化。这种方式称为“传值调用”,即实参向形参传递数据是一种单向传递,实参的值传递给对应的形参,但形参的值不会回传给实参。
例7.1 函数参数之间的单向传递
main()
{
void swap(int,int);
int x=10,y=20;
printf(“(1)x=%d y=%d\n”,x,y);
swap(x,y);
printf(“(4)x=%d y=%d\n”,x,y);
}
void swap(int a,int b)
{
int t;
printf(“(2)a=%d b=%d\n”,a,b);
t=a;a=b;b=t;
printf(“(3)a=%d b=%d\n”,a,b);
}
程序运行结果:
(1)x=10 y=20
(2)a=10 b=20
(3)a=20 b=10
(4)x=10 y=20
当程序执行到调用swap函数时,程序的流程转入swap函数,系统为形参a、b分配临时的存储单元,并将实参的值传给形参存入形参所分配的存储单元。在swap函数中第一次输出时,就是实参传过来的值,如上面的第二行结果。然后将a、b的值进行交换,再进行输出,就是交换以后的结果,如上面的第三行结果。当调用结束后,系统为形参分配的存储单元被释放,不会将改变了的形参值回传给实参。所以在main函数中,再输出x、y的值仍然还是原来的值,如上面的第四行结果。
(2) 被调函数通过return语句将函数值带回到主调函数。
(3) 实参向形参传递数据是按位置对应的传递,不是按名字对应传递。
例7.2 实参向形参按位置传递数据
main()
{
void ex(int z,int y,int x); /* 函数声明 */
int x=10,y=20,z=30;
printf(“(1)x=%d y=%d z=%d\n”,x,y,z);
ex(y,z,x); /* 函数调用 */
}
void ex(int x,int y,int z) /* 函数定义 */
{
printf(“(2)x=%d y=%d z=%d\n”,x,y,z);
}
程序运行结果:
(1)x=10 y=20 z=30
(2)x=20 y=30 z=10
在程序中,函数定义的形参为x、y、z,函数声明的形参为z、y、x,函数声明中的形参名字和函数定义中的形参名字不必一致,实际上函数声明中的形参仅起告诉系统有几个参数的作用,它的名字可以任意,甚至可以缺省,只用形参类型即可。函数调用的实参为y、z、x,按位置对应,即将实参y、z、x的值分别传递给对应的形参x、y、z。这样,在函数ex 中,x、y、z所得到的值就为20、30、10。见上面的输出结果。
(4)当实参表包含多个参数时,对实参表求值的顺序因C语言的版本不同而有所不同,Turbo C是按自右至左的顺序求值,而有的系统是按自左至右的顺序求值。如:
main()
{
int i=2,p;
p=f(i,++i); /* 函数调用 */
printf(“%d\n”,p);
}
int f(int a,int b) /* 函数定义 */
{
int c;
if(a>b)c=1;
else if(a==b)c=0;
else c=-1;
return (c);
}
如果按自右至左的顺序求实参的值(如在Turbo C中),则函数调用相当于f(3,3),程序运行结果为0,如果按自左至右的顺序求实参的值,则函数调用相当于f(2,3),程序运行结果为-1。由于存在上述情况,使程序的通用性受到影响,因此应当避免这种容易引起不同理
解的情况。如将main函数改为:
main()
{
int i=2,j,k,p;
j=i;
k=++i;
p=f(j,k); /* 函数调用 */
printf(“%d\n”,p);
}
则不管是按自右至左的顺序求实参的值,还是按自左至右的顺序求实参的值,程序运行结果均为-1。若将main函数改为:
main()
{
int i=2,j,k,p;
j=++i;
k=i;
p=f(j,k); /* 函数调用 */
printf(“%d\n”,p);
}
则不论采用那种顺序求实参的值,程序运行结果均为0。即将原实参的值在函数调用之前求出赋给另外的变量,通过这些变量作为新实参,而避开求值顺序的影响。
7.5.2 数组名作为参数
当数组名作为函数参数时,要求实参和形参都用数组名(或指针变量,第十章作介绍)。
由于数组名代表数组在内存分配的存储空间的首地址,这种方法称之为“传址调用”方式。当把实参数组名(首地址)传递过去时,由于形参数组名也代表首地址,这样实参数组和形参数组的首地址相同,即实参数组和形参数组占用相同的存储空间。在被调函数中,形参数组中各元素的值如发生变化会使实参数组元素的值同时发生变化。在函数调用结束后,虽然形参数组已不复存在,但实参数组元素的值已发生变化,可以在主调函数中进行使用。例7.3 用选择法对数组中10个整数由小到大排序。
所谓选择法就是先将10个数a[0]到a[9]中最小的数与a[0]对换;再将a[1]到a[9]中最小的数与a[1]对换;……;每进行一轮操作,找出若干个元素中值最小的元素并与参与比较的最前面的那一个元素对换。共进行9轮操作。
下面先写出找出10个数的最小值并与a[0]对换的程序段,即第一轮操作。
i=0;
k=i;
for(j=i+1;j<10;j++)