《程序设计与C语言》课件第5章_第1页
《程序设计与C语言》课件第5章_第2页
《程序设计与C语言》课件第5章_第3页
《程序设计与C语言》课件第5章_第4页
《程序设计与C语言》课件第5章_第5页
已阅读5页,还剩218页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第5章函数

5.1函数的定义5.2函数的调用5.3函数原型及函数声明5.4数据存储类5.5多文件程序中函数和变量的处理5.6递归5.7迭代5.8系统库函数习题55.1函数的定义在C语言中的函数分为两大类:一类是程序员定义的函数,另一类是C标准库中预定义的函数。

C标准库提供了丰富的函数集,这些函数集中的函数能完成常用的数学计算、字符串操作、字符操作、输入/输出等多种有用的操作。使用标准库函数可以节省程序开发的时间,使程序具有更好的可移植性,因此应尽量多地熟悉和掌握ANSIC中的标准库函数。但现实世界是生动复杂的,单用库函数不可能解决所有问题,程序员还必须根据情况自已定义能解决专门问题的新函数,因此必须掌握函数的定义和使用方法。函数由函数头和函数体两部分组成,其格式如下:

〈返回值类型〉〈函数名〉(〈参数列表〉) {声明部分 语句部分

}第一行为函数头,其中的函数名可以是任何合法的标识符,它最好能直观地反映出该函数所完成的任务,以增强程序的可读性。返回值类型是返回给调用者结果的数据类型。如果不指定返回值类型,编译器总假定返回的是int类型。即便如此,明显地写出返回int类型也是一种良好的习惯。参数列表是用逗号分开的参数说明,参数说明的形式是:

〈参数类型〉〈参数名〉对每一个参数都必须作这样的说明,不能在一个类型名后跟多个参数名。函数体由大括号{}括起来,一般由两部分组成:说明部分和语句部分。说明部分声明用于函数内部的临时变量。也可以没有说明部分,只有语句部分。例如,定义求两个浮点数之和的函数:

floatsum(floatx,floaty) {returnx+y; }函数的两个参数x、y被定义成浮点型,返回值的类型也是浮点型。该函数体中没有说明部分,只有语句部分。

【例5-1】编一个求整数位数的函数。intwsh(intn){inti;i=0;while(n){n/=10;i++;}returni;}函数wsh()有一个整型参数n,将返回这个整数n的位数。在该函数体中,i是在本函数内部定义的中间变量。定义函数时系统并不为中间变量分配内存空间,只有在函数被调用时,才为其分配内存,函数调用一结束,该内存即被收回。因此,在函数wsh()的外部不可以对变量i加以引用。函数不一定要有返回值,也可以用来完成某些特定的操作。

【例5-2】编一个函数,它能输出由字母组成的平行四边形,即

A

B

C

D B

C

D

E

C

D

E

F D

E

F

G编程思路:把要输出的行数n作为函数的参数。在输出的每一行中要先输出一定数量的空格,然后输出n个连续的字母,共输出n行。每行行首的空格数随行数而增加。函数定义如下:voidprintnc(intn){inti,j;charc,d;c=d=′A′;for(i=1;i<=n;i++){for(j=1;j<i;j++)putchar(′└┘′);for(j=1;j<=n;j++){putchar(c++); putchar(′└┘′);}putchar(′\n′);c=++d;}}函数的返回值类型void指该函数不返回数据,仅执行打印平行四边形的操作。5.2函数的调用5.2.1函数的参数传递定义函数时,在函数头中出现的参数是形式参数,简称形参。函数调用时,主调函数应当根据被调函数形参的要求提供相应的真实数据,这称为实在参数,简称实参。实参和形参要做到个数相等,类型对应一致。尽管实参和形参可以同名,但为了避免混淆,建议不要使用完全相同的实参和形参名。例如,调用上面定义的sum函数。#include<stdio.h>main(){floatf1,f2;scanf(″%f%f″,&f1,&f2);printf(″%f\n″,sum(f1,f2));return0;}这里main函数是主调函数,sum函数是被调函数,f1,f2是实参。main函数把f1,f2分别交给sum的x和y,sum函数开始工作,把x与y(即f1和f2)的和返回给main函数,并作为printf函数的参数打印出来。

C语言中参数传送的机理是值传送,即主调函数把实在参数的拷贝传给形参后即与形参脱离关系,函数体中对形参的任何处理都已和实参无关了。为了说明这一点,请看下面关于交换两个变量值的函数。

【例5-3】交换两个变量值的函数。voidexchange(inti,intj){intk;printf(″i=%d,j=%d\n″,i,j);k=i;i=j;j=k;printf(″i=%d,j=%d\n″,i,j);}#include<stdio.h>main(){intm=1,n=10;printf(″m=%d,n=%d\n″,m,n);exchange(m,n);

printf(″m=%d,n=%d\n″,m,n);return0;}运行输出:m=1,n=10(函数调用前)i=1,j=10 (函数中参数交换前)i=10,j=1 (函数中参数交换后)m=1,n=10 (函数调用后)可以看出,在exchange函数调用前后主函数中的变量m和n的值没有改变。这说明虽然在函数中发生了参数的交换,但影响不到主调函数。该程序的函数调用示意图如图5-1所示。①k=i;②i=j;③j=k;图5-1例5-3的调用示意图调用时,m、n把值传给i、j后即与之脱离关系,函数中i、j的交换对m、n没有影响。如何让函数实现实参的交换呢?在学习了指针类型之后,这个问题就可以解决了。5.2.2函数的返回值函数的返回值是由return语句实现的。对于任何函数,只要它的返回值类型不是void,那么它都要有个返回值。void是空类型,表示函数不返回值。通过return语句,被调函数将控制权及数值交给主调函数,返回到调用处。函数调用过程如图5-2所示。图5-2函数调用过程示意图main() floatsum(floatx,floaty){ { returnx+y;printf(″…″,sum(f1,f2)); }}被调函数返回的数值类型如何决定呢?如果return语句的表达式类型和函数头部返回值类型不一致时,应该返回哪一个类型呢?答案是应该返回函数头部说明的类型。比如,若sum函数的定义作如下修改:intsum(floatx,floaty){returnx+y;}这时主调函数中接收的应该是int类型,因此printf函数就应该改成下面的形式:

printf(″%d\n″,sum(f1,f2);即输出控制符由′%f′改为′%d′。既然类型为非void的函数都要返回值,main也是个函数,那么它返回一个什么值,又返回给谁呢?如果main函数名前面没有返回类型,则隐含是int类型,所以也应当返回一个整数,不管什么整数都行,它只是表示把控制权返回给调用者,而其具体值并无太大的意义,这就是为什么我们都要在主函数main的最后写上return0的原因。0表示成功返回。主函数的控制权最后返回到操作系统。5.2.3函数的调用方式按被调函数在主调函数中的位置,可以有以下几种调用方式。

1.被调函数作为函数语句单独出现一个函数可看作是一个函数表达式,在其后面加分号即构成函数语句,例如我们经常使用的输入/输出函数

printf(″a=%d,b=%d\n″,a,b);就是一个函数调用语句。前面例子中的

exchange(m,n);也是一个函数调用语句。

2.被调函数作为另一个表达式的一部分例如,求两个数的平均值,可调用函数sum: moyen=sum(m,n)/2;

3.被调函数作为另外一个函数的参数例如,输出两个数的和:

printf(″sum=%f\n″,sum(a,b));如输出四个数a、b、c、d的和,可写为:

printf(″sum4=%f\n″,sum(sum(a,b),sum(c,d));在被调函数中含有返回值的return语句,如果在主调函数中把被调函数作为表达式的一部分,则此时被调函数的返回值是有意义的;而如果把被调函数作为过程语句处理,则主调函数对被调函数的返回值不予理睬,只是把控制权收回来,被调函数的返回值被舍弃。

【例5-4】把用公式求π近似值的计算编成函数,然后调用之。计算中的项数由用户决定。

#include<stdio.h>

doublepi(intt) { inti;

doubles=1.0,sum=0,item=1; for(i=1;i<=t;i++) {sum+=item; s=-s; item=s/(2*i+1); }sum=sum*4;printf(″PI=%f\n″,sum);returnsum;}main(){intn;printf(″Inputtheitemnumber:\n″);

scanf(″%d″,&n);pi(n);return0;}运行输出:Inputtheitemnumber:100↙

PI=3.131593再运行:Inputtheitemnumber:1000↙

PI=3.141493主调函数是将函数pi(n)作为过程语句调用的,因此它没有接收pi()函数的返回值,函数中求得的值已在函数本身输出了。若在主调函数中对pi的调用换一种形式,比如: printf(″pi=%f\n″,pi(n));则pi的返回值就有用了。

【例5-5】求奇特数。输入两个整数a、b,计算并求出一个整数x,使x+a和x+b都是完全平方数。如在某个范围内找不到这样的x,则说明对这一组a、b不存在奇特数,需再输入一组a、b。编程思路:首先根据题意,列出方程:这里已假定x+a是个完全平方数,接着需要验证z也是个完全平方数,如验证成功,则说明这样的奇特数已经找到,并进一步给出z是哪个数的平方。可令①式中的y从1开始逐一增加,通过公式x=y2-a不断地求出x,然后把它代入②式中求出z,再来判断z是否为完全平方数。判断完全平方数的运算可用一个函数来表示。判断时采用下面的方法。根据公式:1+3+5+7+…+(2n-1)=n2

可令x=n2,从x中不断地减去1,3,5,…,最后一定为0,此时函数返回值为1,说明x是一个完全平方数。如果连续减的最后结果不为0,则说明x不是一个完全平方数,返回值为0。程序如下:#include<stdio.h>#defineMAX10000intsquareful(longintx){inti;for(i=1;;i+=2){if((x-=i)<0) break;if(x==0)return1;}return0;}main(){inti,j;longinta,b,x,y,z;do{printf(″Inputa,b:\n″);scanf(″%ld%ld″,&a,&b);

for(y=1;;++y){x=y*y-a;if(x<0)continue;

z=x+b;if(squareful(z)==1)gotolabl;if(y>MAX){printf(″change(a,b)!\n″); break;}}}while(y>MAX);labl:printf(″x=%ld\n″,x);printf(″%ld+%ld=%ld**2\n″,x,a,y);for(j=1,i=1;;i+=2)if((z-=i)!=0)j++;elsebreak;printf(″%ld+%ld=%d**2\n″,x,b,j);return0;}运行输出:Inputa,b:100150↙

change(a,b)!Inputa,b:100200↙

x=476476+100=24**2476+200=26**2在主调函数中调用了判断完全平方数的函数squareful,若该函数值为1,说明已找到一个合乎要求的x,则通过goto语句转到输出这个x的语句。如果y>MAX,说明在MAX范围内无这种奇特数,则通过break语句跳出for循环,再在外层的do_while中输入新一组(a,b)的值。在已确定z是一个完全平方数后,进一步还要确定z是哪一个数的平方,这仍然采用不断从z中减去1,3,5…直到最后结果为0的方法,减一次后计数器j加1,结果为0时的j就是所求的结果,即j2=z。因j定义为int类型,所以输出时使用控制符“%d”,而不是“%ld”。

【例5-6】任何一个非素数,都可以表示成多个素数之积的形式。#include<stdio.h>#include<math.h>voidf(intn){intk,r;printf(″%d=″,n);for(k=2;k<=sqrt(n);k++){r=n%k; while(r==0) {printf(″%d″,k); n/=k; if(n>1)printf(″*″); r=n%k;}}if(n!=1)printf(″%d\n″,n);}main(){intm;printf(″\n″);scanf(″%d″,&m);f(m);return0;}运行结果:

125↙ 125=5*5*5再运行:

348↙ 348=2*2*3*29再运行:

347↙

347=347 //347本身是一个素数

4.函数的嵌套调用

C语言中不能定义嵌套函数,但可以嵌套调用,嵌套调用的示意图见图5-3。执行过程是沿着①、②、③、④、⑤、⑥、⑦、⑧、⑨的路线来进行的,程序始于main()函数,终于main()函数。

【例5-7】输入一个整数,输出其平方与立方。图5-3嵌套调用的示意图我们把求平方和立方的计算编成函数:/*calculatesquare*/longsq(longinti){longa;a=i*i;returna;}/*calculatecube*/longcub(longintj){longb;b=sq(j)*j;returnb;}#include<stdio.h>main(){longintn;printf(″Inputn=?\n″);scanf(″%ld″,&n);printf(″squareof%ldis%ld\n″,n,sq(n));printf(″Cubeof%ldis%ld\n″,n,cub(n));return0;}运行输出:Inputn=?3↙

squareof3is9Cubeof3is27程序中,主函数调用cub函数,而cub函数又调用sq函数,构成嵌套调用。

【例5-8】用弦截法求方程x3-5x2+16x-80=0的根。

编程思路:设f(x)=x3-5x2+16x-80,其曲线如图5-4所示,读入两个数x1和x2,其对应的函数值为f(x1)和f(x2),通过这两点作弦交X轴于x,则x距解x0的距离比x1和x2距x0的距离更近;而如果再通过f(x)和f(x2)作一条弦,则该弦交X轴的点会距x0更近……这样可以一步步地逼近x0。图5-4方程求解示意图在读入x1和x2时应保证方程的根x0在它们之间,如不在它们之间就再读入两个新的x1和x2,反复进行直到符合要求为止。根在x1和x2之间的条件是f(x1)·f(x2)<0,即f(x1)和f(x2)异号。为求弦和X轴的交点,可利用三角形的相似关系,如在图5-4中有Δxx1f(x1)~Δxx2f(x2)又由于|f(x1)|=-f(x1),则比例式为由此公式求出x:进而可求出x处的函数值f(x)。x趋于x0的标志是|f(x)|趋于0,所以可以把|f(x)|<ε作为循环的终止条件。为求解这一问题,我们可以定义下列三个函数:①表示f(x)的函数:给定一个x作参数,返回f(x)的函数值。②求交点的函数:给定x1、x2的参数,返回弦与x轴的交点。③求根的函数:给定x1和x2作参数,返回方程符合条件的根。问题的流程图如图5-5所示。图5-5问题的流程图程序如下:#include<stdio.h>#include<math.h>#defineEPS1.e-3floatf(floatx){floaty;y=((x-5.0)*x+16.0)*x-80; /*把多项式写成乘、加形式,以提高效率*/returny;}floatxpoint(floatx1,floatx2){floatx;x=(x1*f(x2)-x2*f(x1))/(f(x2)-f(x1));/*调用了函数f*/returnx;}floatroot(floatx1,floatx2){floatx,y,y1;y1=f(x1);

do{x=xpoint(x1,x2);y=f(x);if(y*y1>0) /*说明根在后半部*/{y1=y;x1=x;}elsex2=x;/*根在前半部*/}while(fabs(y)>=EPS);returnx;}main(){floatx1,x2,f1,f2,x;do{printf(″Inputx1,x2:\n″);

scanf(″%f%f″,&x1,&x2);

f1=f(x1);f2=f(x2);}while(f1*f2>=0);x=root(x1,x2);printf(″Theroot=%8.4f\n″,x);return0;}运行输出:Inputx1,x2:28↙

Theroot=└┘└┘5.0000在程序中,主函数main调用了root函数,root调用了xpoint函数,xpoint调用了f函数,其调用关系如图5-6所示。图5-6调用关系图上面用流程图描述了执行过程,如果用N-S图来描述,则应注意N-S图中表达do_while的条件表达式的方法。图5-7为用N-S图描述的执行过程。图5-7N-S图描述5.3函数原型及函数声明在上面的函数调用中,我们有意把被调函数都放在了主调函数的前面,为的是在调用时,被调函数的一切信息对主调函数来说都是已知的,因为编译程序是按文本出现的先后次序对程序进行编译的。如果被调函数在后,主调函数在前,则应如何处理呢?这时应该在主调函数之前或在主调函数中对被调函数进行声明。函数声明的一般形式是:

〈返回值类型〉〈函数名〉(〈参数类型声明表〉);其中,〈参数类型声明表〉的形式是:

〈参数类型〉[参数名]],〈参数类型〉[参数名]]…方括号中的内容可以不要,省略号表示可以重复。这就是说在函数声明时,只列出参数类型就可以了,不必再写出参数名,写出是为了清楚,但编译程序会将它忽略。例如,对求和函数sum就可作以下的声明:main(){floatsum(float,float);

…sum();

}floatsum(floatx,floaty){…}如果被调函数在后而又未在主调函数中声明,则编译时会给出错误提示:

″Function′xx′shouldhaveaprototype″(“函数xx应当有个原型”)函数的声明就是函数原型的声明。函数原型提供函数如下的信息:

(1)函数返回值的类型。

(2)函数的参数个数。

(3)各个参数的类型。

(4)各参数之间的顺序。编译器就利用函数原型来检验函数调用,如调用和原型不一致就会给出错误提示,这样可以避免不经检验而执行该函数时可能导致的致命错误或逻辑错误。函数原型的声明地点,可以在所有函数之外,也可以在某个函数之中。如在函数之外,则处在其声明之下的所有函数都可以引用它;若在函数之中,则只有本函数可以引用它。

【例5-9】通过函数调用,求三个正整数的最小公倍数。

编程思路:如果三个数中最大数的倍数能被这三个数整除,则这个倍数就是它们的最小公倍数。先求出三个数的最大值,对这个最大值的倍数循环测试,看能否被这三个数整除。从最小的倍数开始测试,则遇到的第一个符合条件的倍数就是它们的最小公倍数。把求三个数最大值的计算编为一个独立的函数,设该函数的定义在主调函数之后,则在主调函数前应对该函数作原型声明。程序如下:#include<stdio.h>longmax3(long,long,long);main(){longa,b,c,i=1,j,m;printf(″Input3integers:\n″);scanf(″%ld%ld%ld″,&a,&b,&c);m=max3(a,b,c);while(1){j=m*i;if(j%a==0&&j%b==0&&j%c==0)break;i++;}printf(″Result=%ld\n″,j);return0;}longmax3(longx1,longx2,longx3){longmax;if(x1>x2)max=x1;elsemax=x2;if(max<x3)max=x3;returnmax;}运行输出:Input3integers:123236369↙

Result=87084主函数中while(1)表示循环条件永远为真,但能否继续循环下去,还要看循环体中的if条件是否满足,一旦条件满足,则执行break语句退出循环,此时最大数的倍数j即为所求结果。

【例5-10】如果一个整数的所有因子(因子从1开始,不含自身)之和等于其自身,则这个整数就被称为是完数。数学中有一个定理称,如果2n+1-1是一个素数,则2n×(2n+1-1)就一定是完数。编程求10000之内的所有完数。

编程思路:对10000之内所有由2n+1-1生成的整数进行是否为素数的判断,如其为素数,则用2n×(2n+1-1)生成的数就是相应的完数。对每一个完数还应求出它的所有因子。把判断素数及求因子的操作分别设计为两个函数。设函数定义在后,则在主函数中应对它们进行原型声明。程序如下:#include<stdio.h>#include<math.h>main(){intprime(int);voidfactor(intn);intw,m,n=1;m=pow(2,n+1)-1;w=m*pow(2,n);while(w<10000){if(prime(m)){ printf(″%disaperfectnumber\n″,w); printf(″%d=pow(2,%d)*(pow(2,%d+1)-1)n=%d\n″,w,n,n,n); factor(w);}n++;m=pow(2,n+1)-1;w=m*pow(2,n);}printf(″\n\n″);return0;}intprime(intn){inti,q,flag=1;q=sqrt(n);for(i=2;i<=q;i++) if(n%i==0) flag=0;if(flag==1) return1;else return0;}voidfactor(intn){inti,m;m=n/2;printf(″%d=1″,n);for(i=2;i<=m;i++)if(n%i==0&&n!=0){ putchar(′+′); printf(″%d″,i);}printf(″\n″);}运行输出:6isaperfectnumber6=pow(2,1)*(pow(2,1+1)-1)n=16=1+2+328isaperfectnumber28=pow(2,2)*(pow(2,2+1)-1)n=228=1+2+4+7+14496isaperfectnumber496=pow(2,4)*(pow(2,4+1)-1)n=4496=1+2+4+8+16+31+62+124+2488128isaperfectnumber8128=pow(2,6)*(pow(2,6+1)-1)n=68128=1+2+4+8+16+32+64+127+254+508+1016+2032+4064函数原型的一个重要特点是强制类型转换,即把实参的类型强制转换成形参的类型,就好像是实参把其值直接赋给形参类型的变量一样。比如数学库函数sqrt的函数原型中指定参数为double类型,但如用整型数据调用该函数时仍能正确运行。语句

printf(″%.3f\n″,sqrt(4));能正确计算sqrt(4)并打印出值2.000。函数原型通知编译器在把整数4传递给sqrt之前先转换成4.0。通常情况下,在函数调用前,与原型中参数类型不完全一致的实参会按照“提升规则”转换为合适的类型。提升规则把存储空间较小的类型转换成存储空间较大的类型,实际上是建立该值的临时值来使用,原始值并没有被修改。提升规则说明了在不丢失精度的情况下把一种类型转换为其他类型。反之,相反的调用会产生不正确的结果。例如,求整数平方的函数原型为:

intsquare(int);用浮点型实参调用就会返回不正确的结果,如square(4.5),则返回16而非20.25。因此应该避免与提升规则不一致的逆向调用。5.4数据存储类当用户编程上机的时候,编译器会为用户提供一定的内存空间让用户使用。这个内存空间被划分为不同的区域以存放不同的数据。大体上用户区划分为三部分(如图5-8所示):程序区,用来存放用户的程序;动态区,用来存放暂时的数据;静态区,用来存放相对永久的数据。

C语言中的变量都有两个特征:一个是它们的作用范围有大有小;一个是它们的存在期限有长有短。这两个特征统一于它们的存储类,因此我们从存储出发来研究变量的这两方面的特征。程序区动态区静态区图5-8用户区的划分用户区5.4.1自动(auto)变量自动变量的定义位置在复合语句内部。复合语句可以嵌套,函数体就是个最大的复合语句。自动变量在定义时可以加前缀auto关键字或什么都不加。自动变量的作用范围是从定义点起到复合语句的结束。例如下面的程序:#include<stdio.h>main(){autointi=1,j=2;printf(″i=%d,j=%d\n″,i,j);{ inti=3,a=4; floatx=1.1,y=2.2; printf(″i=%d,a=%d\n″,++i,a);

printf(″x=%f,y=%f\n″,x,y);

j++;}printf(″i=%d,j=%d\n″,i,j);return0;}程序中,i、j、x、y、a都是自动变量,只是x、y和a的作用范围是在内嵌的复合语句中,而j的作用范围是整个函数体,其作用力可贯穿内嵌的复合语句。注意变量i,在两个地方都有定义,但它们的作用范围不同:内嵌复合语句中的i只在此复合语句中起作用;而外部的i在整个函数中起作用,但因被内嵌的复合语句中的i所屏蔽,所以其作用达不到复合语句内部。因此这两个i是不同的变量,虽然它们的名字是一样的。所以运行上面的程序,输出结果是:i=1,j=2i=4,a=4x=1.100000,y=2.200000i=1,j=3自动变量放在动态区中,函数调用结束后就被撤销。5.4.2寄存器(register)变量用户频繁使用的数据不一定全要放在内存中,还可以放在运算器的寄存器中。如果数据放在内存中,运算器就要到内存中取数据,运算结束后还要送回内存,这样就需要反复地访问内存,影响运算速度。若把数据直接放在运算器的寄存器中,则可免除频繁访问内存之劳,提高执行效率。定义寄存器变量时前面要加关键字register,如:

registerintri,rj;注意:①寄存器变量的个数是有限的,并且各个系统都不相同。如寄存器变量的个数超过系统提供的数量,则多出来的变量会被作为自动变量处理。②对于有些系统,如TurboC,会把寄存器变量均作为自动变量处理,所以定义register变量意义不大。5.4.3静态(static)变量如果定义变量时在前面加上static修饰,则这样的变量就是静态变量,它被放在内存的静态区。静态变量的使用特征是:在定义时赋初值一次,如不显式赋初值,则系统会对其自动赋初值(数值型变量初值为0,字符型变量初值为空字符′\0′)。包含static变量的函数被调用后,static变量的值并不消失,当再次调用该函数时,上次调用的结果就作为本次的初值使用。在main函数中定义static变量的意义不大,因为程序每次运行都是重新分配空间的。

【例5-11】观察static变量和自动变量的区别。#include<stdio.h>main(){inti;voidaust();for(i=0;i<5;i++)aust();return0;}voidaust(){intau=0;staticintst=0;

printf(″au=%d,st=%d\n″,au,st);++au;++st;}运行输出:au=0,st=0au=0,st=1au=0,st=2au=0,st=3au=0,st=4在函数aust中,静态变量st的初值为0,函数每调用一次,其值增1,并把结果保存到下次调用时。而自动变量au虽然在函数调用时其值也增1,但当函数调用结束后它的值也就消失了。

【例5-12】求下列函数的执行结果。#include<stdio.h>main(){inti,j;intf(int);i=f(3);j=f(5);printf(″i=%d,j=%d\n″,i,j);return0;}intf(intn){staticints=1;while(n)s*=n--;returns;}运行输出:

i=6,j=720函数f()的功能是求参数n的阶乘,即f(n)=n!,main函数对它调用了两次。经过f(3)的调用,使static变量s的值变为6;当第二次调用f(5)时,s的值并不再是从初值1开始,而是从上次调用的结果开始,所以j的值应该是3!*5!=720。

【5-13】输出下面程序的执行结果。#include<stdio.h>main(){inti,m=3;intf(int,int);for(i=1;i<=3;i++)printf(″%d.″,f(i,m));return0;}intf(intx,inty){staticinti=1,m=1;i+=m+2;m=i*x+y;returnm;}运行输出:

7.29.135.注意:这里主调函数和被调函数中出现了同名变量i和m,但它们是不同的变量。被调函数f()中的变量i和m都是局限于此函数中的静态变量,每次调用后的结果都不一样;而主函数main()中的变量i和m在作为参数传递给函数f()时就变成了x和y的值。5.4.4外部变量在所有函数之外定义的变量称为外部变量,它们不为某个函数所专有,程序中所有函数都可以引用它们,因此外部变量也就是全局变量。外部变量放在静态区中。外部变量的使用方法根据它所处位置的不同而不同:

(1)在外部变量自然作用范围之内的函数可以直接引用它们。自然作用范围指从变量的定义点到文件结束。

(2)在自然作用范围之外,即对全局变量的引用在前,而它的定义在后,则应当对它作extern说明。

【例5-14】#include<stdio.h>intmax(intx,inty){returnx>y?x:y;}main(){externinta,b;intmin(int,int);printf(″max=%d\n″,max(a,b));printf(″min=%d\n″,min(a,b));return0;}inta=8,b=5;intmin(inti,intj){returni<j?i:j;}运行输出:max=8min=5在main函数中引用a、b时,a、b尚未定义,因此要做extern说明。为了避免忽略对全局变量的extern说明,一般总把全局变量放在程序的最前面,使所有函数都处在它的自然作用范围之内,所有函数都可以在不做extern说明的情况下引用它们。使用全局变量的好处是可以实现数据在各函数之间的流通,能增加函数间数据联系的渠道,便于写出高效的程序。缺点是任何函数都可以对全局变量进行修改,因此很难判断它的当前值是什么,容易出现错误,因此应该慎重使用。

【例5-15】求调和级数的和,结果用分数表示。编程思路:利用下式求两个分数之和:

把求两个分数之和的运算让函数addrat来完成。在求出一个分数后,还要把它们化简成真分数,这就要找出分子、分母的最大公因子,并用它对分数进行化简,这个计算由函数lowterm完成。在整个操作中都是对分子、分母进行的,若在各个函数中都定义两个分子、分母变量,则互相传递时会很麻烦,因此可以考虑定义两个全局变量num和den代表分子、分母,以它们作为数据纽带把各个函数联系起来。#include<stdio.h>intnum,den;main(){intn,nterm;voidaddrat(int,int);voidlowterm();printf(″Inputthenumberofterms\n″);scanf(″%d″,&n);if(n<=0)printf(″Baddata!\n″);elseif(n==1)printf(″1/1\n″);else{num=1;den=1;for(nterm=2;nterm<=n;nterm++) { addrat(1,nterm); lowterm(); printf(″%d/%d\n″,num,den); }}return0;}voidaddrat(inta,intb){num=num*b+a*den;den=den*b;}voidlowterm(){intnum1,den1,rem;num1=num;den1=den;while(den1!=0){rem=num1%den1;num1=den1;den1=rem;}if(num1>1){num=num/num1;den=den/num1;}}运行输出:Inputthenumberofterms-1↙

Baddata!再运行一次:Inputthenumberofterms1↙

1/1再运行一次:Inputthenumberofterms5↙

3/211/625/12137/60主函数只对全局变量num和den赋了初值1,并没有其他明显的操作,但最后的输出却是经过运算后的值,这主要是借助全局变量num和den在函数addrat和lowterm中进行的,因此主函数显得相当简洁。5.5多文件程序中函数和变量的处理

C语言中的函数定义不能嵌套,所以函数相互之间都是互为外部的,有时说内部函数和外部函数,这主要是针对某个文件而言的。这里需要对C程序的组成作个说明,如图5-9所示。图5-9C程序的组成图5-9说明,一个大一点的C程序可以由多个文件组成,一个文件又可以包含多个函数,包含main函数的文件是主文件,是程序的入口和出口。文件是一个独立的编译单位,而函数不是独立的编译单位。在一个文件中定义的外部变量和函数能否被其他文件所引用,是我们现在要讨论的问题。这里有两种情况:

(1)若一个文件中定义的外部变量和函数不允许其他文件引用,这时应在函数名和变量名的前面加上关键字static。如:staticinta;staticfloatmax(floatx,floaty)这里static声明就把a和max局限在它所定义的文件中,它们只能在这个文件中被使用,不允许其他文件使用,相对于这个文件来说,它们是内部的。

(2)若一个文件中定义的函数和外部变量可以被其他文件引用,这时在函数名和外部变量名前不加static说明,但是凡引用它们的文件必须在各自的文件内部对这些函数和变量作extern说明。比如,在文件f1.c中定义了一个外部变量b和一个函数sum,在文件f2.c中想引用它们,则在f2.c中必须作如下说明:externintb;externintsum(int,int);

【例5-16】输入二元一次方程组中自变量的系数和常数项,解此方程组:编程思路:根据数学知识,可利用系数行列式求解。令则我们把g、x、y的求解用一个函数solve解决,并单独放在一个文件fs.c中,主函数放在文件fm.c中,它调用solve函数并向其提供数据。

文件fm.cexternvoidsolve();inta,b,c,d,e,f;floatg,x,y;#include<stdio.h>main(){printf(″Inputdata:a,b,c,d,e,f:\n″);scanf(″%d%d%d%d%d%d″,&a,&b,&c,&d,&e,&f);solve();if(g==0) printf(″Therearemanysolution!\n″);

else printf(″x=%f,y=%f\n″,x,y);

return0;}

文件fs.cexterninta,b,c,d,e,f;externfloatg,x,y;voidsolve(){g=a*e-b*d;;if(g!=0){x=(c*e-b*f)/g;y=(a*f-c*d)/g;}}运行输出:Inputdata:a,b,c,d,e,f:123456↙

x=-1.000000y=2.000000在文件fm.c中要对在fs.c文件中定义的函数solve作extern说明,同样在文件fs.c中要对在fm.c文件中定义的外部变量a、b、c、d、e、f、g、x、y作extern说明。如何运行由多个文件组成的程序呢?有两种方法:组成扩展名为.prj的项目文件;利用文件包含。

(1)建立项目文件的操作步骤如下(以TurboC编译器为例):①打开“Project/Openproject”对话框。②输入要建立的项目文件名,比如ms.prj。③利用“Project/Additem”命令打开文件清单。④在*.c文件中选择fm.c和fs.c文件并加入到ms.prj文件中。⑤按ESC或Done按钮,则建立项目文件完成。⑥对项目文件像对其他.c文件一样进行编译、连接和运行。⑦在执行结束后,应执行“Project/CloseProject”操作,否则会影响后面其他文件的编译。

(2)文件包含方法是指,在一个文件中用编译预处理命令#include将本文件中所需的其他文件包含进来为我所用。比如在fm.c文件中加入 #include″fs.c″这条预处理命令后,就可以直接对fs.c文件进行编译、连接、运行,这时fs.c文件已是fm.c的一部分,所以在fm.c中就不需要再对fs.c文件中的solve函数作extern说明了。同样在fs.c中也不需要对fm.c文件中定义的外部变量作extern说明了。

(3)VC++环境下建立项目文件的步骤。在VC中,任何C文件都被组织到项目(或工程,即project)中,而项目又是在工作区(workspace)中运行的。一个工作区中可以有多个项目,一个项目又可以有多个C文件,其结构形式为:工作区和项目可以由用户建立,也可以自动生成。由用户建立时,其顺序是先建立工作区,再建立项目,最后再建立文件。前面只介绍了建立文件的过程,没有提到建立工作区和项目,实际上,它们已由编译系统自动生成了。工作区名和项目名均默认地与文件名同名,但那是一个程序只有一个文件的情况。如果一个程序由多个文件组成,则必须显式地建立项目文件,并把组成该程序的所有文件都组织到项目中。其步骤如下:

(1)打开“File/New”菜单,在New(新建)对话框中选择Project(工程)标签,在左边的列表框中选择“Win32ConsoleApplication”选项,即选择控制台工作方式。然后在右边的“Projectname”一栏输入项目名,它下面的“Location”栏用来指定项目所在的目录,如图5-10所示。图5-10New对话框

(2)在输入项目名(此处为fms)并指定其目录后,按“OK”按钮,并在后面出现的对话框中连续按回车键,则会出现如图5-11所示的窗口。其左部窗口中显示出“Workspace′fms′:1project[s]”,下一行是“fmsfiles”,这说明项目fms已被加到工作区fms中(这里工作区默认地采用了项目的名字)。项目中包含三个文件夹:SourceFiles(源程序文件)、HeaderFiles(头文件)和ResourceFiles(资源文件)。项目是所有用于创建一个可运行程序的文件的集合。项目文件的扩展名是.dsp,不可以用type命令显示其内容。图5-11项目窗口

(3)执行菜单“Project/AddToProject/Files...”,如图5-12所示。这是为了要向项目文件中添加.c程序。在弹出的对话框“InsertFilesintoProject”中,选择要加入到项目中的.c文件,如图5-13所示。图5-12添加.c程序图5-13选择.c程序

(4)单击“OK”按钮,此时如打开左边项目fms的SourceFiles文件夹,可以发现已有两个源程序文件fm.c和fs.c加入到了项目文件中。对项目文件不需要编译,直接执行“Build/Buildfms.exe”(或按F7),可生成可执行文件.exe。该命令自动进行编译和连接操作,并将编译、连接的结果显示在屏幕下方的调试信息窗口中,如图5-14所示。图5-14显示调试信息

(5)执行菜单“Build/!Executefms.exe”(或按Ctrl+F5),运行该可执行文件,结果如图5-15所示。图5-15运行结果5.6递归迄今为止,我们所用的函数调用都是以层次的方式用一个函数调用另一个函数,但某些问题可以用调用函数自身的方法来解决。递归函数就是直接或间接调用自身的函数。5.6.1递归函数用递归解决问题,就是把问题分为两部分:一部分属于基本问题,对它可以直接求出结果;另一部分虽然不是基本问题,但是与原问题类似并且比原问题简单一些。对稍微简单一些的问题继续进行分解,直至达到边界条件。解决完边界处的问题(即基本问题)后,函数会沿着调用顺序不断给上一次的调用返回结果,直至原始问题。这看起来似乎很复杂,但事实上是个很自然的过程。递归函数用来解决递归问题。所谓递归问题,就是能逐渐简化到边界条件的问题。来看下面的例子。

【例5-17】求阶乘的运算:

n!=1(n=0或n=1) n!=n(n-1)!(n≥2)当n大于等于2的时候,欲求n的阶乘,只要先求出n-1的阶乘,把所得结果乘以n即为n的阶乘。至于n-1的阶乘是多少,在这一步可先不考虑,设它为p。则

n!=n·p这是一步就可求出的问题,但这时p还不是一个确定的数,还要等待返回结果。接下来求p=(n-1)!,再把它进行分解。它等于n-1去乘n-2的阶乘,即p=(n-1)·(n-2)!,设(n-2)!=q,则p=(n-1)·q。接下来的问题是再去求q。按照上述方法继续类似的操作,问题一步步地化简,最终一定能达到求1的阶乘的地步,这时就称达到了边界条件,可以直接求解了。任何递归调用都有一个边界条件,否则递归调用将会无限地进行下去。在上面的计算中,每一步都要等待下一步的计算结果,因此它当时的状态就得先保存起来,等下步的结果返回后再进行基本运算。当到达边界条件后,可求出确定的值,于是返回到上一步,而上一步当时的状态已被保存,只等有返回值后就可进行直接运算,它运算的结果又继续向上一步返回,这样一步步向上返回,直至原始问题得解。从上面的分析可以看出,求解递归问题有两个过程:第一个过程是递归调用,从原始问题出发,层层向下,直至边界条件;第二个过程是返回过程,从边界条件出发,层层向上返回,直至原始问题。通过递归调用图,就可以清楚地看到这两个过程。

【例5-18】求4!的递归调用,调用图如图5-16所示。

4!

4!=24 ↓ ↑返回4!=24(等待返回3!)4×3! 4×6 ↓ ↑返回3!=6(等待返回2!)3×2! 3×2 ↓ ↑返回2!=2 2×1!边界条件2×1(递归调用过程) (返回过程)图5-16求4!的递归调用图下面是求n的阶乘的程序:#include<stdio.h>longintfac(int);main(){inti;for(i=1;i<=10;i++)printf(″%2d!=%ld″,i,fac(i));return0;}longintfac(intn){if(n<=1)return1;elsereturnn*fac(n-1);}运行输出:1!=12!=23!=64!=245!=1206!=7207!=50408!=403209!=36288010!=3628800

说明:①从输出结果看,阶乘增加得非常快,7以上的阶乘就超过了int类型的取值范围,这就是为什么要把返回值的类型定义为longint的原因,相应的输出格式是′%ld′。如果要求更大数的阶乘,可以选用float或double作为阶乘函数返回值类型。②求阶乘函数中有个核心语句:双分支的if语句。其中的一个分支描述的是边界条件,另一个分支是递归调用。在递归调用中又出现了该函数的函数名,但这里的参数比函数头中的参数就简单了一些,这就是递归函数的书写模式。对于大多数递归函数,都可以这样考虑和书写。在递归函数体中还可以出现不只一个的递归调用,它们各自有不同的参数。

【例5-19】求Fibonacci数列:1,1,2,3,5,8,13,…观察数列,可发现这样的规律:从第3项开始,每一项都是其前面两项之和。设fib(n)[JP]表示第n个Fibonacci数,则有:

fib(n)=1(n=1或n=2) fib(n)=fib(n-1)+fib(n-2)(n≥3)

Fibonacci数列是个很有趣的数列,随着项数的增加,前后相邻两项的比值趋向于0.618,这个比值被称为“黄金分割”。建筑师经常用黄金分割来指导设计门窗、房间和建筑物等,人们认为符合黄金分割的物体是一种美的物体。

Fibonacci函数的数学表达式已清楚地表明这是个递归问题,因此可以很容易地用C语言写出它的递归函数。#include<stdio.h>longfib(int);main(){longbf;intn;printf(″Enteraninteger:\n″);scanf(″%d″,&n);bf=fib(n);printf(″Fibonacci(%d)=%ld″,n,bf);return0;}longfib(intm){if(m==1‖m==2)return1;elsereturnfib(m-1)+fib(m-2);}运行输出:Enteraninteger:8↙

Fibonacci(8)=21

fib函数的递归调用如图5-17所示(这里是求fib(5))。图5-17求fib(5)的递归调用

对符合边界条件的基本问题可直接求出其解。这里涉及到对操作数求值的次序问题。在fib(3)=fib(2)+fib(1)中,是先计算fib(2)还是先计算fib(1),ANSIC标准对此没有作出明确的规定,因此程序可对调用顺序不作任何假定。对本程序和大多数程序而言,计算结果都是正确的,但在某些程序中,操作数的运算顺序可能会影响表达式的最终结果。在C语言的所有运算符中,ANSIC只规定了下面四种运算符对操作数的求值顺序:&&、‖、逗号和?:。前三个运算符是双目运算符,按从左到右的顺序计算它的两个操作数。最后一个是C语言中惟一的一个三目运算符,它最左边的操作数优先运算。至于其他运算符操作数的求值顺序,不同的系统有不同的规定。因此如果编写的程序依赖于操作数的求值顺序,将可能导致错误,因为编译器的执行顺序不一定会和编程者的想法一致。另外,递归函数的调用次数也是个值得注意的问题。以本题为例,每次递归调用fib函数都要再递归地调用两次,因此当n很大时,对fib函数的调用次数将是惊人的,所以应该避免编写像fib函数这样的递归函数,即使要编写,参数也不应很大。

【例5-20】把一个正整数的各位数字以字符形式输出。#include<stdio.h>voiddigit(int);main(){intm;printf(″Inputaninteger:\n″);scanf(″%d″,&m);digit(m);return0;}voiddigit(intn){if(n==0)return;else{digit(n/10);putchar(n%10+′0′);}}

digit()是个递归函数,其参数为n,而在该函数中又以n/10为参数进行递归调用,则调用是向参数减少的方向变化的。当n变为0时,达到边界条件,递归调用结束,开始回退。在回退的过程

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论