第5章函数.docx
《第5章函数.docx》由会员分享,可在线阅读,更多相关《第5章函数.docx(36页珍藏版)》请在冰豆网上搜索。
第5章函数
第5章函数
C程序的重要组成是函数。
当你面临一个大的问题时,为了有效地解决问题,方法是将问题分解。
在设计一个大的程序时,关键的一点是把它划分成若干个小而容易处理的程序模块,每一个程序模块用来实现一个特定的功能,这样的程序模块在高级语言中一般称为子程序,在C语言中则称为函数。
本章主要讲授函数的定义和函数的调用方式与方法,以及变量的作用域和存储类型等。
C语言的函数定义都是互相平行的、独立的,但可以嵌套调用和递归调用,调用时实参与形参之间是值传递。
一个C程序是由若干个(至少一个)函数组成,每一个函数用来实现一个特定的功能,通过互相调用完成系统的总功能。
C语言的函数提供了实现“自上而下”的结构化编程方法,图5-1是一个程序中函数调用的示意图。
每一个C程序有且只有一个main函数,它的运行是从main函数开始,一个结构良好的程序最后从main函数结束。
一个C程序的所有函数可以组织在若干个源文件中,源文件是编译的基本单位。
图5-1函数调用示意图
C语言的变量,从作用域(即变量在什么范围内有效)来讲,可以分为局部变量和全局变量;从生存期(即变量值在内存中存在的时间)来讲,可以分为动态存储变量和静态存储变量。
5.1函数的定义
从用户使用的角度看,函数有两种:
(1)标准函数,即库函数。
这是由系统提供的,像printf()和sqrt()等都是库函数,对于这些库函数用户不必自己定义,可以直接使用它们。
(2)用户定义函数,是用户根据所需解决的问题而专门编写的函数。
先给出一个简单的函数调用的例子。
例5.1用户定义函数的定义与调用。
#include
intsum(){/*用户定义函数sum*/
intn,hj=0;
for(n=1;n<=100;n++)
hj=hj+n;
return(hj);/*返回函数值*/
}
voidoutput(intzh){
/*用户定义函数output,形参变量zh接受实参s传过来的值*/
printf(************\n);/*调用库函数printf*/
printf(sum=%d\n,zh);
printf(************\n);
}
voidmain(){
ints;
s=sum();/*调用用户定义函数sum*/
output(s);/*调用用户定义函数output*/
}
运行结果如下:
************
sum=5050
************
这里,sum、output都是用户自定义的函数名,分别用来求1至100之间的自然数之和,输出所求的结果。
包含函数调用语句“s=sum();”和“output(s);”的函数称为主调函数,函数sum及output均称为被调用函数。
由上例可知,一个函数由函数头和函数体两部分构成。
函数头又包括函数类型、函数名、形式参数及其类型声明等组成;函数体由声明部分和执行语句部分构成。
在函数体的声明部分中,对函数中所用的变量进行定义,另外还需对函数中将要调用的函数作声明(见5.3.2节)。
函数定义的一般形式如下:
(1)无参函数的定义形式:
类型标识符函数名(){——函数头(即函数的说明部分)
函数体
声明部分
执行语句
}
例5.1中的sum函数是无参函数。
函数名前的“类型标识符”是用来声明函数的类型,即函数返回值的类型。
return语句的作用是将它后面的表达式hj的值返回到主调函数中。
(2)有参函数定义的一般形式:
类型标识符函数名(形式参数列表){——函数头(即函数的说明部分)
函数体
声明部分
执行语句
}
例5.1中的output函数是有参函数。
变量zh为形式参数,“intzh”定义形参变量zh的类型。
主调函数中的函数调用语句“output(s);”中的变量s为实际参数。
由于该函数不需要返回值,所以函数类型为void。
“形式参数列表”列出所有形式参数及其类型声明,如果有一个以上形式参数,则它们之间用逗号分隔。
在函数调用时,主调函数中的“实际参数列表”中的每一个实际参数值依次传递给被调用函数中的“形式参数列表”中的每一个形参变量。
(3)空函数,它的形式为:
类型标识符函数名(){
}
例如:
voidcount(){
}
调用此函数时,什么工作也不做,没有起任何实际作用。
但在主调函数中写上“count();”语句表明“在这里要调用一个函数”,而现在这个函数的具体内容尚未写出来,因此调用此函数时没有起任何实际作用,待以后扩充函数功能时补充上该函数的具体内容。
在程序设计中往往是根据需要确定若干模块,分别由一些函数来实现。
在编写程序的开始阶段,可以在将来准备扩充功能的地方写上一个空函数,只是这些函数尚未编写好,先占一个位置,待以后再编写函数的具体内容,这样做可以使程序的结构清楚,可读性好,以后扩充功能方便,对程序结构影响不大。
说明:
(1)一个C程序由多个函数组成,这些函数可以组织在一个源文件中,也可以组织在多个源文件中。
每个源文件由一个或多个函数组成,它可以分别编写、分别编译,提高调试效率。
一个源文件可以为多个C程序公用。
(2)每一个C程序有且仅有一个main()函数,不是每一个源文件都必须包含main()函数。
C程序的执行从main函数开始,调用其他函数后流程回到main函数,一个结构良好的程序最后在main函数中结束整个程序的运行。
main函数是系统定义的。
(3)main函数可以调用其他函数,但其他函数不能调用main函数。
除main函数以外的其他函数可以互相调用。
(4)所有函数都是平行的。
首先表现在定义函数时每一个函数都是一个独立的程序段,在位置安排上它们是互相独立的,一个函数并不从属于另一个函数,即函数不能嵌套定义;其次表现在函数调用时除main函数外它们是可以互相调用的。
(5)在调用库函数时,要用预编译命令“#include”将声明库函数的“头文件”包括到用户源文件中。
预编译命令“#include”将在第12章中介绍。
5.2函数参数与函数值
5.2.1形式参数和实际参数
在调用函数时,大多数情况下,主调函数都需要将有关数据传递给被调用函数。
这就是前面提到的有参函数。
在定义函数时函数名后面圆括弧中的“形式参数列表”中的每一个变量为一个“形式参数”,简称为“形参”;在主调函数的调用函数语句中,函数名后面圆括弧中的“实际参数列表”中的每一个表达式为一个“实际参数”,简称为“实参”。
例5.2函数的参数与函数。
#include
floatvalue(floatx,floaty){/*定义有参函数value,形参为实型变量x、y*/
floatz;
z=x*y;
return(z);/*返回函数值:
z*/
}
voidmain(){
floatw,je;
printf(Pleaseinputweight:
);
scanf(%f,&w);
je=value(w,5.2);/*函数调用时的实参为w和5.2*/
printf(Value=%.2f\n,je);
}
运行情况如下:
Pleaseinputweight:
3.4
Value=17.68
例5.2通过函数调用,使两个函数中的数据发生联系,如图5-2所示。
je=value(w,5.2)
(main函数)
floatvalue(floatx,floaty){
floatz;
z=x*y;
return(z);
}
(value函数)
图5-2主调函数与被调函数之间的数据传递关系
关于形参与实参的说明:
(1)实参可以是常量、变量或表达式,如:
value(w+2.6,5.2);
但要求它们有确定的值。
形参则只能是变量,用来接受实参传过来的值,而且在函数的“形式参数列表”中要对形参变量的类型进行定义。
C语言规定,实参到形参变量的数据传递是“值传递”,即实参与形参是按传值方式相结合的,也称为传值调用(callbyvalue)方式。
传值调用的过程是:
①形参变量占用独立于实参的存储空间。
如果实参是常量或一般表达式,则实参不占内存存储空间;如果实参是变量,实参变量需占用内存存储空间。
形参变量是一定要占用内存存储空间的,且与实参变量占用的是不同的内存存储空间。
②形参的存储空间是在函数被调用时才分配的。
调用开始,系统为形参开辟一个临时存储区,然后将各实参之值传递给形参,这时形参就得到了实参的值。
这种虚实结合方式称为“值结合”。
③函数调用结束返回时,为形参变量所开辟的临时存储区也被释放。
“值传递”的特点是:
函数中对形参变量的操作不会影响到主调函数中的实参变量,即实参与形参之间的值传递是单向传递,只由实参传递给形参变量,而不能由形参变量传回给实参变量。
也就是说,在执行一个被调用函数时,形参变量的值如果发生改变,并不会改变主调函数的实参变量的值。
这点与PASCAL等其他高级语言不同。
例5.3函数的值传递。
#include
voidsort(intx,inty){
intz;
printf(x=%d,y=%d\n,x,y);
if(x>y){
z=x;x=y;y=z;
}
printf(x=%d,y=%d\n,x,y);
}
voidmain(){
inta,b;
scanf(%d%d,&a,&b);
printf(a=%d,b=%d\n,a,b);
sort(a,b);
printf(a=%d,b=%d\n,a,b);
}
运行情况如下:
85
a=8,b=5
x=8,y=5
x=5,y=8
a=8,b=5
该程序在运行过程中,各变量的存储单元分配及其取值的变化过程如图5-3所示。
图5-3例5.3执行时的值传递过程示意图
在主函数中定义了变量a和b并赋值,在函数sort未被调用时,函数sort中定义的变量z及形参变量x、y并不占内存中的存储单元,见图5-3(a);主函数调用函数sort,给形参变量x和y分配存储单元,并将实参对应的值传递给形参,同时给函数sort中的变量z分配存储单元,见图5-3(b);函数sort执行if语句后,各变量的取值情况见图5-3(c);被调函数sort调用结束后,形参变量x、y及变量z所占用的存储单元被释放,实参变量a、b的存储单元仍保留并维持原值,见图5-3(d)。
函数sort中定义的变量z及形参变量x、y的作用域是函数sort,其生存期是函数sort被调用期间。
有关变量的作用域及生存期的概念在5.4、5.5节中介绍。
(2)实参与形参的类型应一致,否则将发生“类型不匹配”而得到错误的结果。
字符型与整型可以互相通用。
(3)实参与形参的个数应相等。
如果实参个数多于形参个数,则实参与形参按顺序对应一一传递数据,多余的实参的值不被传递;反之,如果实参个数少于形参个数,则实参与形参按顺序对应一一传递数据,多余的形参没有赋值。
(4)在老版本C语言中,对形参类型的声明是放在函数定义的第2行,也就是不在第1行的括号内指定形参的类型,而在括号外单独指定,例如:
floatvalue(floatx,floaty){
…
}
相当于:
floatvalue(x,y)
floatx,y;
{…}
5.2.2函数的返回值
在调用函数时,一般情况下,不仅主调函数需要将有关数据传递给被调用函数,而且亦希望将被调用函数的计算结果返回给主调函数,这就是函数的返回值。
(1)函数的返回值是通过函数中的return语句实现的。
return语句中断函数的执行,并将程序流程控制返回到主调函数。
如果return语句后面跟了一个表达式,那么被调用函数中的该表达式的值也返回给主调函数,该表达式可以用圆括号括起来,亦可以不用圆括号括起来;如果return语句后面没有跟表达式,则return语句仅起将程序流程控制返回到主调函数的作用,不起返回值的作用。
通过return语句最多只能返回一个确定的值给主调函数,不能实现返回一个以上的值给主调函数。
(2)在一个函数中可以没有或者有若干个return语句。
如果不存在return语句,那么在程序运行到函数体的右花括号时,控制才返回到主调函数,这称为“到底返回”,或称“末端退出”。
如果一个函数中有多个return语句,则第一次执行到的return语句中断函数的执行,把控制返回到主调函数。
(3)函数值的类型。
返回值的类型由函数定义时指定的函数类型决定,而不是由return语句后面的表达式值的类型决定。
如:
intvalue(floatx,floaty){
returnx*y;
}
表达式x*y的类型是实型,但函数value返回值的类型是整型。
以函数类型决定返回值的类型,对数值型数据,可以自动进行类型转换,不同类型之间的自动转换与变量赋值时的转换原则相同。
return语句后面的表达式的类型一般应该同函数的类型相一致。
C语言规定,凡不加类型声明的函数,一律自动按整型处理。
例5.1的函数sum的返回值为整型,因此可不必声明。
(4)如果被调用函数中没有return语句或return语句中没有跟一个表达式,函数并不是不返回值,而是不返回一个确定的、用户所希望得到的函数值,返回的是一个不确定的值。
(5)为了明确表示“不返回值”,需要用“void”定义“无类型”(或称“空类型”)。
例5.1中的函数output就是被定义为void类型。
为了使程序减少出错,保证正确调用,凡不要求返回函数值的函数,一般应定义为“void”类型。
5.3函数的调用
5.3.1函数调用的方式
函数的调用是通过写出它的函数名和圆括号内的“实际参数列表”来实现的。
函数调用的一般写法为:
函数名(实际参数列表)
如果“实际参数列表”包含多个实参表达式,各实参表达式之间需用逗号隔开。
如果是调用无参函数,则“实际参数列表”就没有,调用写法变为:
函数名()
函数调用可以出现在任一适合有表达式的地方。
如果函数不打算通过return语句后面的表达式来返回一个值的话,它可以作为一个语句来调用,即
函数名(实际参数列表);
按函数在函数调用语句中出现的位置来分,可以有以下三种函数调用方式:
(1)函数语句。
把函数调用直接作为一个语句。
如例5.1中的
output(s);
(2)函数表达式。
函数调用出现在一个表达式中,函数作为表达式的因子,这种表达式称为函数表达式。
这时要求函数返回一个确定的值以参加表达式的运算。
例如:
je=value(w,5.2)+5.0;
(3)函数参数。
函数调用作为另一个函数的实参。
例如:
m=max(max(a,b),max(c,d));
其中max(c,d)和max(a,b)分别是一次函数调用,它们的值作为max另一次调用的实参。
如果函数max的功能是返回两个数中较大的一个,则m的值是a、b、c、d四个数中最大的。
又如:
printf(Value=%f\n,value(w,5.2));
也是把函数调用value(w,5.2)作为库函数printf的一个参数。
函数调用的含义是:
(1)对“实际参数列表”中的每一个表达式求值。
(2)将实参表达式的值依次对应地赋给在被调函数头部分定义的形参变量。
(3)执行被调用函数的函数体。
(4)如果有return语句被执行,那么控制返回到主调函数中。
如果return语句中包含表达式,同时将return语句中表达式的值返回到主调函数中。
(5)如果缺少return语句,那么在运行到函数体末尾时控制自动返回到主调函数中。
对“实际参数列表”中各实参表达式的求值顺序,TurboC规定是:
自右而左。
例5.4TurboC实参表达式的计算顺序。
#include
intmax(intx,inty){
intz;
z=(x>y)?
x:
y;
returnz;
}
voidmain(){
intc,i=5,j=6;
c=max((++i,i+j),(i++,i++,j--));
printf(c=%d\n,c);
}
运行结果如下:
c=13
按自右至左的顺序求实参表达式的值,首先求逗号表达式(i++,i++,j--)的值,逗号表达式的值为6,求完逗号表达式的值后i和j的值分别为7和5;然后求逗号表达式(++i,i+j)的值,逗号表达式的值为13,求完逗号表达式的值后i和j的值分别为8和5。
因此,函数调用语句“c=max((++i,i+j),(i++,i++,j--));”的两个实参表达式的值分别为13和6,分别传递给形参变量x和y。
5.3.2对被调用函数的声明
在一个函数(主调函数)中调用另一个函数(被调用函数)需要具备哪些条件呢?
(1)首先被调用的函数必须是已经存在的函数,它可以是库函数,也可以是用户定义函数。
但光有这一点还不够,一般还需对被调用函数进行声明。
(2)如果使用库函数,一般应该在本文件开头用#include命令将被调用库函数所在的头文件包含到本文件中来。
如:
#include
在头文件stdio.h中包含了输入输出库函数所用到的一些宏定义信息。
有关宏定义等概念详见第12章。
(3)如果使用用户定义函数,而且主调函数与被调用函数在同一个源文件中,一般应该在主调函数中对被调用函数进行声明,即向编译系统声明将要调用此函数,并将有关信息通知编译系统。
例5.5对被调用函数作声明。
#include
voidmain(){
floatw,je;
floatvalue(floatx,floaty);/*对被调用函数value进行声明*/
printf(Pleaseinputweight:
);
scanf(%f,&w);
je=value(w,5.2);
printf(Value=%.2f\n,je);
}
floatvalue(floatx,floaty){
floatz;
z=x*y;
return(z);
}
运行情况如下:
Pleaseinputweight:
3.4
Value=17.68
注意:
对函数的“定义”和“声明”不是一回事。
“定义”是指对函数功能的确立,包括指定函数名、函数类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。
而“声明”的作用则是把函数名字、函数类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时进行检查,例如,检查函数名字、函数类型是否正确,实参与形参的类型和个数是否一致。
从程序中可以看到,对函数的声明与函数定义中的第1行(函数头)基本上是相同的。
因此,可以简单地照写已定义的函数头部分,再加一个分号,就成为了对函数的“声明”。
事实上,在函数声明中也可以不写形参变量名,而只写形参变量的类型。
如:
floatvalue(float,float);
在C语言中,以上的函数声明称为函数原型(prototype)。
使用函数原型是ANSIC的一个重要特点。
它的作用主要是在程序的编译阶段对被调用函数的合法性进行检查。
对于例5.5,main函数位于value函数之前,而在编译时是从上到下逐行进行的,如果在main函数中没有对函数value的声明,当编译到对value函数进行调用的语句“je=value(w,5.2);”时,编译系统不知道value是不是函数名,也无法判断实参表达式的类型和个数是否正确,因而无法进行正确性检查。
只有在运行时才会发现实参表达式与形参变量之间的类型或个数不一致,出现运行错误。
如果在主调函数中用函数原型对被调用函数进行了声明,则编译系统就记下了被调用函数的有关信息,从而在对函数调用语句“je=value(w,5.2);”进行编译时,就可以根据函数的原型对被调用函数的合法性进行检查。
如果被调用函数与函数原型不一致,则会导致编译出错,它属于语法错误。
函数原型的一般形式为:
类型标识符函数名(形参类型1,形参类型2,…)
或
类型标识符函数名(形参类型1形参变量1,形参类型2形参变量2,…)
前者是基本形式。
为了便于程序的阅读,也可以在函数原形中加上形参变量名,如后者所示。
但编译系统并不检查形参变量名,因此形参变量名是什么都无所谓,不要求它与函数定义中的形参变量名相同。
在函数原型的声明中,函数类型、函数名、参数个数、参数类型和参数顺序都必须与函数定义的函数头中的一致;函数调用语句中的函数名、实参表达式个数也要求与函数原型声明中的一致;实参表达式的类型必须与函数原型中的形参类型一致。
读者可能会发现:
在例5.1至例5.4的main函数中并未对被调用函数进行声明。
C语言规定,在以下两种情况下,主调函数中可以不对被调用函数进行类型声明。
(1)如果被调用函数的定义出现在主调函数之前,可以不必进行声明。
因为编译系统已经先知道了已定义的函数类型,会自动对函数调用语句进行正确性检查。
(2)如果已在所有函数定义之前,在文件的开头,在函数的外部已做了函数声明,则在各个主调函数中均不必对所调用的函数再作原型声明。
例如:
floatvalue(float,float);/*在所有函数之前进行函数声明*/
intmax(int,int);
voidmain(){
…/*不必对它所调用的函数进行声明*/
}
floatvalue(floatx,floaty){/*定义函数value*/
…
}
intmax(intx,inty){/*定义函数max*/
…
}
例5.6每输入一个学生的4门课程的成绩,计算并显示该学生4门课程的平均成绩。
#include
#defineSTUDCOUNT3
#defineSCORECOUNT4
voidmain(){
floataverage(int,int),aver;
inti;
for(i=1;i<=STUDCOUNT;i++){
aver=average(i,SCORECOUNT);
printf(***Number%d***\n,i);
printf(\tAveragescoreis%5.1f\n,aver);
}
}
floataverage(intnum,intcount){
floatscore,sum=0;
inti;
printf(Pleaseinput%dscoresofnumber%d:
\n,count,num);
for(i=1;i<=count;i++){
scanf(%f,&score);
sum+=score;
}
return(sum/count);
}
运行情况如下:
Pleaseinput4scoresofnumber1:
75
80
90
85
***Number1***
Averagescoreis82.5