第7章函数内容提要函数定义、函数调用、函数原型、函数的参数传递与返回值递归函数和函数的递归调用函数封装,函数复用,函数设计的基本原则,程序的健壮性变量的作用域与存储类型,全局变量、自动变量、静态变量、寄存器变量“自顶向下、逐步求精”的模块化程序设计方法数学中的函数()yfx自变量因变量函数名程序设计中的函数程序设计中的函数不局限于计算–计算类,如打印阶乘表的程序……–判断推理类,如排序、查找……参数返回值问题的提出读多少行的程序能让你不头疼?假如系统提供的函数printf()由10行代码替换,那么你编过的程序会成什么样子?实际上一个printf()有上千行代码main()中能放多少行代码?如果所有代码都在main()中,怎么团队合作?如果代码都在一个文件中,怎么团队合作?“事无巨细”,“事必躬亲”X7.1分而治之与信息隐藏分而治之(DivideandConquer,Wirth,1971)把较大的任务分解成若干个较小的任务,并提炼出公用任务:函数信息隐藏(InformationHiding,Parnas,1972)设计得当的函数可把具体操作细节对程序中无需知道它们的那些部分隐藏掉,从而使整个程序结构清楚使用函数时,不用知道函数内部是如何运作的,只按照我们的需要和它的参数形式调用它即可7.2函数(function)的定义函数是模块化编程的最小单位若干相关的函数可以合并成一个“模块”一个C程序由一个或多个源程序文件组成一个源程序文件由一个或多个函数组成7.2.1函数的分类函数生来都是平等的,互相独立的,没有高低贵贱和从属之分main()稍微特殊一点点C程序的执行从main函数开始调用其他函数后流程回到main函数在main函数中结束整个程序运行7.2.1函数的分类标准库函数ANSI/ISOC定义的标准库函数符合标准的C语言编译器必须提供这些函数函数的行为也要符合ANSI/ISOC的定义第三方库函数由其它厂商自行开发的C语言函数库不在标准范围内,能扩充C语言的功能(图形、网络、数据库等)自定义函数自己定义的函数包装后,也可成为函数库,供别人使用7.2.2函数定义(Functiondefinition)类型函数名(类型参数1,类型参数2,……){声明语句序列可执行语句序列return表达式;}返回值类型函数名标识符,说明运算规则参数表相当于运算的操作数返回运算的结果函数出口7.2.2函数定义(Functiondefinition)类型函数名(类型参数1,类型参数2,……){声明语句序列可执行语句序列return表达式;}函数体的定界符参数表里的变量(叫形式参数,FormalParameter)也是内部变量函数体7.2.2函数定义(Functiondefinition)void函数名(void){声明语句序列可执行语句序列return;}函数无返回值,用void定义返回值类型用void定义参数,表示没有参数return语句后无需任何表达式【例7.1a】计算整数n的阶乘n!/*函数功能:用迭代法计算n!函数入口参数:整型变量n表示阶乘的阶数函数返回值:返回n!的值*/longFact(intn)/*函数定义*/{inti;longresult=1;for(i=2;i=n;i++){result*=i;}returnresult;}返回值类型函数名说明函数的功能返回值作为函数调用表达式的值形参表,函数入口函数内部可以定义只能自己使用的变量,称内部变量函数名(表达式1,表达式2,……);实际参数(ActualArgument)函数调用(FounctionCall)时提供的表达式中的参数有返回值时放到一个数值表达式中c=max(a,b);作为另一个函数调用的参数c=max(max(a,b),c);printf(%d\n,max(a,b));无返回值时函数调用表达式display(a,b);返回值=函数名(实参表列);函数名(实参表列);7.3向函数传值和从函数返回值函数的参数传递实参和形参必须匹配数目一致,类型一一对应(否则会发生自动类型转换)【例7.1】例:交换两个数据。#includestdio.hvoidmain(){voidswap(int,int);inta,b;a=2,b=6;swap(a,b);printf(“a=%d,b=%d”,a,b);}a2b6voidswap(intx,inty){inttemp;temp=x;x=y;y=temp;}x2y662a、b并未被交换。函数的参数和返回值实参和形参必须类型匹配形参也是一个变量,在函数被调用的时候分配空间当发生函数调用的时候,实参的值传递给形参通过返回值函数将值传回给调用的函数,返回值的类型由函数的返回类型决定一个函数最多只能有一条return语句例:四舍五入#includestdio.hintround(floatx){x=x+0.5;returnx;}voidmain(){floata=2.7,b=3.4;inti,j;i=round(a);j=round(b);printf(“%i,%i\n”,i,j);}ij2.73.4ab2.7x3.23.43.9337.3.2函数原型(FunctionPrototype)在调用函数前先声明其返回值类型、函数名和参数函数原型有助于编译器对函数参数类型的匹配检查末尾有一个分号,声明时不要省略形参和返回值的类型【例7.1】函数定义与函数声明的区别函数定义指函数功能的确立指定函数名、函数类型、形参及类型、函数体等是完整独立的单位函数声明是对函数名、返回值类型、形参类型的说明不包括函数体是一条语句,以分号结束,只起一个声明作用7.3.3函数封装(Encapsulation)函数封装使得外界对函数的影响仅限于入口参数,而函数对外界的影响仅限于一个返回值和数组、指针类型的参数【例7.1】Why?传入负数的实参会怎样?#includestdio.hlongFact(intn);voidmain(){intm;longret;printf(inputm:);scanf(%d,&m);ret=Fact(m);printf(%d!=%ld\n,m,ret);}longFact(intn){inti;longresult=1;for(i=2;i=n;i++){result*=i;}returnresult;}防御性程序设计(DefensiveProgramming)如何使函数具有遇到不正确使用或非法数据输入时避免出错的能力,增强程序的健壮性?在函数的入口处,检查输入参数的合法性【例7.2】计算整数n的阶乘n!longFact(intn){inti;longresult=1;if(n0){printf(Inputdataerror!\n);}else{for(i=2;i=n;i++)result*=i;returnresult;}}如何使函数具有遇到不正确使用或非法数据输入时避免出错的能力,增强程序的健壮性?在函数的入口处,检查输入参数的合法性防御性程序设计(DefensiveProgramming)【例7.2】计算整数n的阶乘n!主函数如何修改?增加对函数返回值的检验防御性程序设计(DefensiveProgramming)【例7.3】计算整数n的阶乘n!传入负数的实参时Fact()会返回-1吗?存在死代码的原因何在?防御性程序设计(DefensiveProgramming)【例7.3】计算整数n的阶乘n!如何修改这个缺陷?如何保证不会传入负数实参?防御性程序设计(DefensiveProgramming)【例7.2】计算整数n的阶乘n!【例7.4】编写计算组合数的程序函数复用7.3.4函数设计的基本原则信息隐藏1函数规模要小2函数功能要单一3函数接口定义要清楚入口参数有效性检查敏感操作前的检查调用成功与否的检查函数的嵌套调用嵌套调用–在调用一个函数的过程中,又调用另一个函数C语言规定函数不能嵌套定义,但可以嵌套调用–函数是相互平行的main(){……a();}a函数{b();…return;}b函数{……return;}①③④⑤⑥⑦②7.4递归函数(RecursionFunction)7.4.1递归的概念在函数当中又出现直接或者间接的调用该函数本身——c的一个特色直接递归:fun1(){…fun1();…}间接递归:fun1(){…fun2();…}fun2(){…fun1();…}例:有5个人坐在一起,问第5个人多少岁?他说比第4个人大2岁。问第4个人岁数,他说比第3个人大2岁。问第3个人,又说比第2个人大2岁。问第2个人,说比第1个人大2岁。最后问第1个人,他说是10岁。请问第5个人多大。age(5)=age(4)+2age(4)=age(3)+2age(3)=age(2)+2age(2)=age(1)+2age(1)=10用数学公式表述如下:age(n)=10(n=1)age(n)=age(n-1)+2(n1)intage(intn)/*求年龄的递归函数*/{intc;/*c用作存放函数的返回值的变量*/if(n==1)c=10;elsec=age(n-1)+2;return(c);}运行结果如下:18#includestdio.hvoidmain(){printf(“%d”,age(5));}7.4.2递归调用(RecursiveCall)longfact(intn){if(n0)return-1;elseif(n==0||n==1)return1;elsereturnn*fact(n-1);}【例7.6】计算n!=n*(n-1)*(n-2)*…*1函数直接或间接调用自己无需考虑n0了7.4.2递归调用(RecursiveCall)unsignedlongfact(unsignedintn){if(n==0||n==1)return1;elsereturnn*fact(n-1);}基线情况(basecase)一般情况(generalcase)【例7.6】计算n!=n*(n-1)*(n-2)*…*1递归调用其实是一种算法问题的解决需要通过有规律的调用同样的算法直到某些已知值或直接值通常可以抽象为函数:f(x)=a,c=truef(x’),c=false{7.4.2递归调用(RecursiveCall)递归调用应该能够在有限次数内终止递归递归调用若不加以限制,将无限循环调用必须在函数内部加控制语句,仅当满足一定条件时,递归终止,称为条件递归任何一个递归调用程序必须包括两部分递归循环继续的过程递归调用结束的过程if(递归终止条件成立)return递归公式的初值;elsereturn递归函数调用返回的结果值;n!=n*(n-1)!(n-1)!=(n-1)*(n-2)!(n-2)!..(n-3)!5!:4!=4*3!3!=3*2!2!=2*1!1!=1回推过程递推过程每个递归函数必须至少有一个基线条件一般情况必须最终能简化为基线条件递归层数太多易导致栈空间溢出后果很严重,程序被异常中止fact(5)=5*fact(4)=120fact(4)=4*fact(3)=24fact(3)=3*fact(2)=6fact(2)=2*fact(1)=2fact(1)=1mainfact(5)fact(4)fact(3)fact(2)fact(1)递归与迭代迭代即循环方法来编写的阶乘函数unsignedlongFact(unsignedintn){unsignedlongresult=1;unsignedinti;for(i=1;i=n;i++)result*=i;returnresult;}递归程序遵循了数学中对阶乘的定义因此递归方法编写程序具有更清晰、可读性更好的优点递归与迭代1,1,2,3,5,8,......110)2()1(10)(