《C程序设计函数》PPT课件.ppt

上传人:za****8 文档编号:16086118 上传时间:2020-09-18 格式:PPT 页数:191 大小:717.52KB
收藏 版权申诉 举报 下载
《C程序设计函数》PPT课件.ppt_第1页
第1页 / 共191页
《C程序设计函数》PPT课件.ppt_第2页
第2页 / 共191页
《C程序设计函数》PPT课件.ppt_第3页
第3页 / 共191页
资源描述:

《《C程序设计函数》PPT课件.ppt》由会员分享,可在线阅读,更多相关《《C程序设计函数》PPT课件.ppt(191页珍藏版)》请在装配图网上搜索。

1、8.1 概述 8.2 函数定义的一般形式 8.3 函数参数和函数的值 8.4 函数的调用 8.5 函数的嵌套调用 8.6 函数的递归调用 8.7 数组作为函数参数 8.8 局部变量和全局变量 8.9 变量的存储类别 8.10 内部函数和外部函数 8.11 如何运行一个多文件的程序 习题,第8章 函 数,8.1 概述,一个较大的程序一般应分为若干个程序模块,每一个模块用来实现一个特定的功能。所有的高级语言中都有子程序这个概念,用子程序实现模块的功能。在语言中,子程序的作用是由函数完成的。一个程序可由一个主函数和若干个函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多

2、个函数调用任意多次。图8.1是一个程序中函数调用的示意图。,在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。要善于利用函数,以减少重复编写程序段的工作量。 先举一个简单的函数调用的例子。例8.1 main() printstar();* 调用printstar函数 * print-message();* 调用print message */ printstar(); * 调用printstar函数 * printstar() *printstar函数* ,printf(* * * * * * * * * * * * * * * * * *n); print-messag

3、e() * print-message函数* printf(How do you do!n); 运行情况如下: * * * * * * * * * * * * * * * * * * How do you do! * * * * * * * * * * * * * * * * * *,图8.1,printstar和print-message都是用户定义的函数名,分别用来输出一排“*”号和一行信息。 说明: (1) 一个源程序文件由一个或多个函数组成。一个源程序文件是一个编译单位,即以源程序为单位进行编译,而不是以函数为单位进行编译。 (2) 一个程序由一个或多个源程序文件组成。对较大的程序,一

4、般不希望全放在一个文件中,而将函数和其他内容(如预定义)分别放在若干个源文件中,再由若干源文件组成一个C程序。这样可以分别编写、分别编译,提高调度效率。一个源文件可以为多个C程序公用。,(3) 程序的执行从main函数开始,调用其他函数后流程回到main函数,在main函数中结束整个程序的运行。main函数是系统定义的。 (4) 所有函数都是平行的,即在定义函数时是互相独立的,一个函数并不从属于另一函数,即函数不能嵌套定义(这是和PASCAL不同的),函数间可以互相调用,但不能调用main函数。 (5) 从用户使用的角度看,函数有两种: 标准函数,即库函数。这是由系统提供的,用户不必自己定义这

5、些函数,可以直接使用它们。应该说明,不同的C系统提供的库函数的数量和功能不同,当然有一些基本的函数是共同的。, 用户自己定义的函数。用以解决用户的专门需要。 (6) 从函数的形式看,函数分两类: 无参函数。如例8.1中的printstar和print-message就是无参函数。在调用无参函数时,主调函数并不将数据传送给被调用函数,一般用来执行指定的一组操作(例如,例8.1那样),printstar函数的作用是输出18个星号。无参函数可以带回或不带回函数值,但一般以不带回函数值的居多。 有参函数。在调用函数时,在主调函数和被调用函数之间有数据传递。也就是说,主调函数可以将数据传给被调用函数使用

6、,被调用函数中的数据也可以带回来供主调函数使用。,类型标识符函数名() 声明部分 语句 例8.1中的printstar和print-message函数都是无参函数。用“类型标识符”指定函数值的类型,即函数带回来的值的类型。无参函数一般不需要带回函数值,因此可以不写类型标识符,例8.1就如此。,8.2 函数定义的一般形式1. 无参函数的定义形式,类型标识符函数名(形式参数表列) 声明部分 语句 例如: int max(int x,int y) int ;*函数体中的声明部分 xy?xy; return(); ,2. 有参函数定义的一般形式,这是一个求x和y二者中大者的函数,笫1行第一个关键字in

7、t表示函数值是整型的。max为函数名。括号中有两个形式参数x和y,它们都是整型的。在调用此函数时,主调函数把实际参数的值传递给被调用函数中的形式参数x和y。花括弧内是函数体,它包括声明部分和语句部分。在声明部分定义所用的变量,此外对将要调用的函数作声明(见8.4.3节)。在函数体的语句中求出的值(为x与y中大者),return(z)的作用是将的值作为函数值带回到主调函数中。return后面的括弧中的值(z)作为函数带回的值(或称函数返回值)。在函数定义时已指定max函数为整型,在函数体中定义为整型,二者是一致的,将作为函数max的值带回调用函数(见例8.2)。,如果在定义函数时不指定函数类型,

8、系统会隐含指定函数类型为int型。因此上面定义的max函数左端的int可以省写。 3. 可以有“空函数” 它的形式为 类型说明符函数名() 例如: dummy() 调用此函数时,什么工作也不做,没有任何实际作用。在主调函数中写上“dummy();” 表明 “这里,要调用一个函数”, 而现在这个函数没有起作用, 等以后扩充函数功能时补充上。 在程序设计中往往根据需要确定若干模块, 分别由一些函数来实现。 而在第一阶段只设计最基本的模块, 其他一些次要功能或锦上添花的功能则在以后需要时陆续补上。在编写程序的开始阶段,可以在将来准备扩充功能的地方写上一个空函数(函数名取将来采用的实际函数名(如用me

9、rge()、matproduct()、oncatenate()、shell()等,分别代表合并、矩阵相乘、字符串连接、希尔法排序等),只是这些函数未编好,先占一个位置,以后用一个编好的函数代替它。这样做,程序的结构清楚,,可读性好,以后扩充新功能方便,对程序结构影响不大。空函数在程序设计中常常是有用的。 4. 对形参的声明的传统方式 在老版本C语言中,对形参类型的声明是放在函数定义的笫2行,也就是不在笫1行的括号内指定形参的类型,而在括号外单独指定,例如上面定义的max函数可以写成以下形式: int max(x,y) /* 指定形参x,y */ int x,y; /* 对形参指定类型 */ i

10、nt z;,z = x y ? x: y; return(z); 一般把这种方法称为传统的对形参的声明方式,而把前面介绍过的方法称为现代的方式。Turbo C和目前使用的多数C版本对这两种方法都允许使用,两种用法等价,NSI新标准推荐前一种方法,即现代方式。它与PASCAL语言中所用的方法是类似的。本书中的程序采用新标准推荐的现代方式。但由于有些过去写的书籍和程序使用传统方式,因此读者应对它有所了解,以便能方便地阅读它们。,8.3.1 形式参数和实际参数 在调用函数时,大多数情况下,主调函数和被调用函数之间有数据传递关系。这就是前面提到的有参函数。前面已提到:在定义函数时函数名后面括弧中的变量

11、名称为“形式参数”(简称“形参”),在主调函数中调用一个函数时,函数名后面括弧中的参数(可以是一个表达式)称为“实际参数”(简称“实参”)。 例8.2调用函数时的数据传递。 main() int a,b,c;,8.3 函数参数和函数的值,scanf(d,d,a,b); cmax(a,b); printf(ax isd,c); max(int x,int y)定义有参函数max int ; xy?xy; return(); ,运行情况如下: 7,8 ax is 8 程序中第712行是一个函数定义(注意第7行的末尾没有分号)。第7行定义了一个函数名max和指定两个形参x、y及其类型。程序第4行是一

12、个调用函数语句,max后面括弧内的a、b是实参。a和b是main函数中定义的变量,x和y是函数max中的形式参数。通过函数调用,使两个函数中的数据发生联系。见图8.2。,图8.2 关于形参与实参的说明: (1) 在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元。只有在发生函数调用时,函数max中的形参才被分配内存单元。在调用结束后,形参所占的内存单元也被释放。,(2) 实参可以是常量、变量或表达式,如: max(3,ab); 但要求它们有确定的值。在调用时将实参的值赋给形参(如果形参是数组名,则传递的是数组首地址而不是数组的值。请参阅8.7节)。 (3) 在被定义的函数中

13、,必须指定形参的类型(见例8.2程序第7行)。 (4) 实参与形参的类型应相同或赋值兼容。例8.2中实参和形参都是整型,这是合法的、正确的。如果实参为整型而形参x为实型,或者相反,则按第2章介绍的不同类型数值的赋值规则进行转换。例如实参值a为3.5,而形参x为整型,则将实数3.5转换成,整数3,然后送到形参b。但此时应将max函数放在main函数的前面或在main函数中对被调用函数max作原型声明,否则会出错。关于对函数的声明见8.4.3小节。字符型与整型可以互相通用。 (5) 语言规定,实参变量对形参变量的数据传递都是“值传递”,即单向传递,只由实参传给形参,而不能由形参传回来给实参,这是和

14、TN不同的。在内存中,实参单元与形参单元是不同的单元。如图8.3所示。,图8.3 图8.4 在调用函数时,给形参分配存储单元,并将实参对应的值传递给形参,调用结束后,形参单元被释放,实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数的实参的值。例如,若在执行函数过程中x和y的值变为10和15,而a和b仍为2和3,见图8.4。,8.3.2 函数的返回值 通常,希望通过函数调用使主调函数能得到一个确定的值,这就是函数的返回值。例如,例8.2中,max(2,3)的值是3,max(5,2)的值是5。赋值语句将这个函数值赋给变量c。下面对函数值作一些说明:

15、(1) 函数的返回值是通过函数中的return语句获得的。return语句将被调用函数中的一个确定值带回主调函数中去。见图8.2中从return语句返回的箭头。如果需要从被调用函数带回一个函数值(供主调函数使用),被调用函数中必须包含return语句。如果不需要从被调用函数带回函数值可以不要return语句。,一个函数中可以有一个以上的return语句,执行到哪一个return语句,哪一个语句起作用。 return语句后面的括弧也可以不要,如return ;它与“return();”等价。 return后面的值可以是一个表达式。例如,例8.2中的函数max可以改写如下: max(int x,i

16、nt y) return(xy?xy); 这样的函数体更为简短,只用一个return语句就把求值和返回都解决了。,(2) 函数值的类型。既然函数有返回值,这个值当然应属于某一个确定的类型,应当在定义函数时指定函数值的类型。例如: int max(float x,float y)/* 函数值为整型 */ char letter(char c1,char c2) /* 函数值为字符型 */ double min(int x,int y) /* 函数值为双精度型 */ 读者会问:例8.2中的函数定义并没有说明其类型,为什么?语言规定,凡不加类型说明的函数,一律自动按整型处理。例8.2中的max函数返

17、回值为整型,因此可不必说明。,在定义函数时对函数值说明的类型一般应该和return语句中的表达式类型一致。例如,例8.2中用隐含方式指定max函数值为整型,而变量也被指定为整型,通过return语句把的值作为max的函数值,由max带回主调函数。的类型与max函数的类型是一致的,是正确的。 (3) 如果函数值的类型和return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。即函数类型决定返回值的类型。 例8.3返回值类型与函数类型不同。将例8.2稍作改动(注意是变量的类型改动)。,main() float a,b; int c; scanf(f,f,a,b);

18、cmax(a,b); printf(axisdn,c); max(float x,float y); float ;/* z为实型变量 */ xy?xy; return(); ,运行情况如下: 15, 25 ax is 2 函数max定义为整型,而return语句中的为实型,二者不一致,按上述规定,先将转换为整型,然后max(x,y)带回一个整型值2回主调函数main。如果将main函数中的c定义为实型,用f格式符输出,也是输出2000000。 有时,可以利用这一特点进行类型转换,如在函数中进行实型运算,希望返回的是整型量,可让系统去自动完成类型转换。但这种做法往往使程序,不清晰,可读性降低,

19、容易弄错,而且并不是所有的类型都能互相转换的(如实数与字符类型数据之间)。因此建议初学者不要采用这种方法,而应做到使函数类型与return返回值的类型一致。 (4) 如果被调用函数中没有return语句,并不带回一个确定的、用户所希望得到的函数值,但实际上,函数并不是不带回值,而只是不带回有用的值,带回的是一个不确定的值。例如,在例8.1程序中,尽管没有要求printstar和print-message函数带回值,但是如果在程序中出现下面的语句也是合法的:,int a,b,c; aprintstar(); bprint-message(); cprintstar(); printf(d,d,d

20、n,a,b,c); 运行时除了得到和例8.1一样的结果外,还可以输出a、b、c的值(今为21、20、21)。a、b、c的值不一定有实际意义(今printstar函数输出21个字符,返回值为21,print-message输出20个字符,返回值为20)。,(5) 为了明确表示“不带回值”,可以用“void”定义“无类型”(或称“空类型”)。例如,例8.1中的定义可以改为 void printstar() void print-message() 这样,系统就保证不使函数带回任何值,即禁止在调用函数中使用被调用函数的返回值。如果已将printstar和print-message函数定义为void类

21、型,则下面的用法就是错误的:,aprintstar(); bprint-message(); 编译时会给出出错信息。 为使程序减少出错,保证正确调用,凡不要求带回函数值的函数,一般应定义为void类型。许多语言书的程序中都大量用到void类型函数,读者应对此有一定了解。,8.4.1函数调用的一般形式 函数调用的一般形式为函数名(实参表列);如果是调用无参函数,则“实参表列”可以没有,但括弧不能省略,见例8.1。如果实参表列包含多个实参,则各参数间用逗号隔开。实参与形参的个数应相等,类型应一致。实参与形参按顺序对应,一一传递数据。但应说明,如果实参表列包括多个实参,对实参求值的顺序并不是确定的,

22、有的系统按自左至右顺序求实参的值,有的系统则按自右至左顺序。许多版本(例如Turbo和 )是按自右而左的顺序求值。,8.4函数的调用,例8.4 main() int i2,p; pf(i,i); /* 函数调用 */ printf(d,p); int f(int a,int b) /* 函数定义 */ int c; if(ab)c1; else if(ab)c0; else c1; return(c); ,在Turbo C系统上运行的结果为:0 如果按自左至右顺序求实参的值,则函数调用相当于f(2,3),程序运行应得结果为“1”。若按自右至左顺序求实参的值,则它相当于f(3,3),程序运行结果

23、为“0”。读者可以在所用的计算机系统上试一下,以便知道它所处理的方法。由于存在上述情况,使程序通用性受到影响。因此应当避免这种容易引起不同理解的情况。如果本意是按自左而右顺序求实参的值,可以改写为 i; ki; pf(,k);,如果本意是自右而左求实参的值,可改写为 ji; pf(j,j); 这种情况在printf函数中也同样存在,如 printf(d,d,i,i); 也发生上述同样的问题,若i的原值为3,在Turbo C上运行结果为4,3。请读者务必注意,应该避免这种容易混淆的用法。,8.4.2 函数调用的方式 按函数在程序中出现的位置来分,可以有以下三种函数调用方式: 1 函数语句 把函数

24、调用作为一个语句。如例8.1中的printstar();这时不要求函数带回值,只要求函数完成一定的操作。 2 函数表达式 函数出现在一个表达式中,这种表达式称为函数表达式。这时要求函数带回一个确定的值以参加表达式的运算。例如:c2*max(a,b);函数max是表达式的一部分,它的值乘2再赋给c。,3. 函数参数 函数调用作为一个函数的实参。例如:mmax(a,max(b,c);其中max(b,c)是一次函数调用,它的值作为max另一次调用的实参。m的值是a、b、c三者最大的。又如:printf (%d, max (a,b);也是把max(a,b)作为printf函数的一个参数。 函数调用作为

25、函数的参数,实质上也是函数表达式形式调用的一种,因为函数的参数本来就要求是表达式形式。,8.4.3 对被调用函数的声明和函数原型 在一个函数中调用另一函数(即被调用函数)需要具备哪些条件呢? (1) 首先被调用的函数必须是已经存在的函数(是库函数或用户自己定义的函数)。但光有这一条件还不够。 (2) 如果使用库函数,一般还应该在本文件开头用include命令将调用有关库函数时所需用到的信息“包含”到本文件中来。例如,前几章中已经用过的include 其中“studioh”是一个“头文件”。在studioh文件中放了输入输出库函数所用到的一些宏定义信息。如果不包含“studioh”文件中的信息,

26、就无法使用输入输出,库中的函数。同样,使用数学库中的函数,应该用include .h是头文件所用的后缀,标志头文件(header file)。有关宏定义等概念请见第8章。 (3) 如果使用用户自己定义的函数,而且该函数与调用它的函数(即主调函数)在同一个文件中,一般还应该在主调函数中对被调用的函数作声明,即向编译系统声明将要调用此函数,并将有关信息通知编译系统。“声明” 一词的原文是declaration,过去在许多书中译为“说明”, 近年来,愈来愈多的计算机专家提出应称为声明,作者也认为称为“声明”更确切,表意更清楚。,例8.5对被调用的函数作声明。 main() float add(flo

27、at x, float y);*对被调用函数的声明* float a,b,c; scanf(f,f,a,b); cadd(a,b); printf(sum isf,c); float add(float x,float y)/*函数首部* float ; /* 函数体 */ xy; return(); ,运行情况如下: 3.6, 6.5 sum is 10.000000 这是一个很简单的函数调用,函数add的作用是求两个实数之和,得到的函数值也是实型。请注意程序第2行:float add(float x, float y);是对被调用的add函数作声明。注意:对函数的“定义”和“声明”不是一回

28、事。“定义”是指对函数功能的确立,包括指定函数名,函数值类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。而“声明” 的作用则是把函数的名字、函数类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查(例如函数,名是否正确,实参与形参的类型和个数是否一致)。从程序中可以看到对函数的声明与函数定义中的第1行(函数首部)基本上是相同的。因此可以简单地照写已定义的函数的首部,再加一个分号,就成为了对函数的“声明”。 其实,在函数声明中也可以不写形参名,而只写形参的类型。如:float add(float,float);在C语言中,把以上形式的函数声明称为函数原

29、型(function prototype)。使用函数原型是N C的一个重要特点。它的作用主要是利用它在程序的编译阶段对调用函数的合法性进行全面检查。从例8.5中可以看到main函数的位置在add函数的前面,而在进行如果,没有对函数的声明,当编译到包含函数调用的语句“c=add(a,b)”;时,编译系统不知道add是不是函数名,也无法判断实参(a和b)的类型和个数是否正确,因而无法进行正确性的检查。只有在运行时才会发现实参与形参的类型或个数不一致,出现运行错误。但是在运行阶段发现错误并重新调试程序,是比较麻烦的,工作量也较大。 应当在编译阶段尽可能多地发现错误,随之纠正错误。现在我们在函数调用之

30、前用函数原型做了函数声明。因此编译系统记下了所需调用的函数的有关信息,在对“c=add(a,b)”;进行编译时就“有章可循”了。编译系统根据函数的原型对函数的调用的编译时是从上到下逐行进行的,合法性进行全,面的检查。和函数原型不匹配的函数调用会导致编译出错。 它属于语法错误。用户根据屏幕显示的出错信息很容易发现和纠正错误。 函数原型的一般形式为 (1) 函数类型 函数名(参数类型1, 参数类型2) (2) 函数类型 函数名(参数类型1, 参数名1, 参数类型2, 参数名2) 第(1)种形式是基本的形式。为了便于阅读程序,也允许在函数原型中加上参数名,就成了第(2)种形式。但编译系统不检查参数名

31、。因此参数名是什么都无所谓。上面程序中的声明也可以写成,float add(float a, float b);/* 参数名不用x、y,而用a、b */ 效果完全相同。 应当保证函数原型与函数首部写法上的一致,即函数类型、函数名、参数个数、参数类型和参数顺序必须相同。函数调用时函数名、实参个数应与函数原型一致。实参类型必须与函数原型中的形参类型赋值兼容,按第2章介绍的赋值规则进行类型转换。如果不是赋值兼容,就按出错处理。,说明: (1) 以前的C版本的函数声明方式不是采用函数原型,而只声明函数名和函数类型。例如在例8.5中也可以采用下面的函数声明形式 float add( ); 不包括参数类型

32、和参数个数。系统不检查参数类型和参数个数。新版本也兼容这种用法,但不提倡这种用法,因为它未进行全面的检查。 (2) 实际上,如果在函数调用之前,没有对函数作声明,则编译系统会把笫一次遇到的该函数形式(函数定义或函数调用)作为函数的声明,并将函数类型默认为int型。例如例8.2在调用max函数,之前没有进行函数声明,编译时首先遇到的函数形式是函数调用“max(a,b)”, 由于对原型的处理是不考虑参数名的,因此系统将max()加上int作为的函数声明,即 int max( ); 因此,不少C教材说,如果函数类型为整型,可以在函数调用前不必作函数声明。但是使用这种方法时,系统无法对参数的类型做检查

33、。若调用函数时参数使用不当,在编译时也不会报错。因此,为了程序清晰和安全,建议都加以声明为好。例如在例8.2中最好加上以下函数声明: int max(int,int); 或 int max(int x,int y);,(3) 如果被调用函数的定义出现在主调函数之前,可以不必加以声明。因为编译系统已经先知道了已定义的函数类型,会根据函数首部提供的信息对函数的调用作正确性检查。 如果把例8.5改写如下(即把main函数放在add函数的下面),就不必在main函数中对add声明。 float add(float x,float y) float; xy; return(); main()*不必对ad

34、d函数作声明*, float a,b,c; scanf(f,f,a,b); cadd(a,b); printf(f,c); (4) 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必对所调用的函数再作声明 。例如: char letter(char, char); *以下3行在所有函数之前,且在函数外部* float f(float, float);,int i(float, float); main() /*不必声明它所调用的函数* char letter(char c1,char c2) /*定义letter函数*/ float f(float x,float y

35、) /*定义f函数*/ int i(float ,float k)/*定义i函数*/ ,除了以上(2)(3)(4) 所提到的三种情况外,都应该按上述介绍的方法对所调用函数作声明,否则编译时就会出现错误。用函数原型来声明函数,还能减少编写程序时可能出现的错误。由于函数声明的位置与函数调用语句的位置比较近,因此在写程序时便于就近参照函数原型来书写函数调用,不易出错。 8.5 函数的嵌套调用 语言的函数定义都是互相平行、独立的,也就是说在定义函数时,一个函数内不能包含另一个函数,这是和PASCAL不同的(PASCAL允许在定义一个函数时,其函数体内又包含另一个函数的完整定义,这就是嵌套定义。这个内嵌

36、的函数只能被包含它的函数所调用,其他函数不能调用它)。,语句不能嵌套定义函数,但可以嵌套调用函数,也就是说,在调用一个函数的过程中,又调用另一个函数。见图8.5。,图8.5,图8.5表示的是两层嵌套(连main函数共3层函数),其执行过程是: (1) 执行main函数的开头部分; (2) 遇函数调用a的操作语句,流程转去a函数; (3) 执行a函数的开头部分; (4) 遇调用b函数的操作语句,流程转去函数b; (5) 执行b函数,如果再无其他嵌套的函数,则完成b函数的全部操作; (6) 返回调用b函数处,即返回a函数; (7) 继续执行a函数中尚未执行的部分,直到a函数结束;,(8) 返回ma

37、in函数中调用a函数处; (9) 继续执行main函数的剩余部分直到结束。 例8.6用弦截法求方程x3-5x2+16x-80=0的根。 方法如下: (1) 取两个不同点x1,x2,如果f(x1)和f(x2)符号相反,则(x1,x2)区间内必有一个根。如果f(x1)与f(x2)同符号,则应改变x1,x2,直到f(x1)、f(x2)异号为止。注意x1、x2的值不应差太大,以保证(x1,x2)区间内只有一个根。 (2) 连接f(x1)和f(x2)两点,此线(即弦)交x轴于x,见图8.6。,x点坐标可用下式求出: x=x1f(x2)-x2f(x1)f(x2)-f(x1) 再从x求出f(x). (3)

38、若f(x)与f(x1)同符号,则根必在(x,x2)区间内,此时将x作为新的x1。如果f(x)与f(x2)同符号,则表示根在(x1,x)区间内,将x作为新的x2。,图8.6,图8.7,(4) 重复步骤 (2) 和 (3) , 直到 f(x) 为止, 为一个很小的数, 例如 10-6. 此时认为 f(x)0.根据上述思路画出N-S流程图,见图8.7。 分别用几个函数来实现各部分功能: (1) 用函数f(x)来求x的函数:x3-5x2+16x-80. (2) 用函数xpoint (x1,x2)来求f(x1)和f(x2)的连线与x轴的交点x的坐标。 (3) 用函数root (x1,x2)来求(x1,x

39、2)区间的那个实根。显然,执行root函数过程中要用到函数xpoint,而执行xpoint函数过程中要用到f函数。,请读者先分析下面的程序。 #include float f(float x) * 定义f函数,以实现f(x)x3-5x2+16x-80 * float y; y(x50)*x160)*x800; return(y); float xpoint(float x1,float x2) *定义xpoint函数,求出弦与x轴交点 */, float y; y=(x1*f(x2)x2*f(x1)(f(x2)f(x1); return(y); float root(float x1,floa

40、t x2) /*定义root函数,求近似根 * int i;,float x,y,y1; y1f(x1); do xxpoint(x1,x2); yf(x); if(y*y10) f(x)与f(x1)同符号 * y1y; x1x; else x2x;,while(fabs(y)00001); return(x); main() /*主函数*/ float x1,x2,f1,f2,x; do printf(input x1,x2:n); scanf(“f,f”,x1,x2); f1f(x1);,f2f(x2); while(f1*f20); xroot(x1,x2); printf( root

41、of equation is84f,x); 运行情况如下: input x1,x2: 2,6 root of equation is50000,从程序可以看到: (1) 在定义函数时,函数名为f、xpoint、root的3个函数是互相独立的,并不互相从属。这3个函数均定为实型。 (2) 3个函数的定义均出现在main函数之前,因此在main函数中不必对这3个函数作类型说明。 (3) 程序从main函数开始执行。先执行一个do-while循环,作用是:输入x1和x2,判别f(x1)和f(x2)是否异号,如果不是异号则重新输入x1和x2,直到满足f(x1)与f(x2)异号为止。然后用函数调用roo

42、t(x1,x2)求根x。调用root函数过程中,要调用xpoint函数来求f(x1)与fx2)连线的交点x。在调用xpoint函数过程中要用到函数f来求x1和x2的相应的函数值f(x1)和f(x2)。这就是函数的嵌套调用。见图8.8。,图8.8,(4) 在root函数中要用到求绝对值的函数fabs,它是对实型数求绝对值的标准函数。它属于数学函数库,故在文件开头用include即把使用数学库函数时所需用到的有关信息包含进来。 8.6 函数的递归调用 在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。语言的特点之一就在于允许函数的递归调用。例如:,int f(int x)

43、 int y,; f(y); return(2*); 在调用函数f的过程中,又要调用f函数,这是直接调用本函数,见图8.9。下面是间接调用本函数。,图 8.9 图8.10 在调用f1函数过程中要调用f2函数,而在调用f2函数过程中又要调用f1函数,见图8.10。 从图上可以看到,这两种递归调用都是无终止的自身调用。显然,程序中不应出现这种无终止的递归调用,而只应出现有限次数的、有终止的递归调用,这可以用if语句来控制,只有在某一条件成立时才继续执行递归调用,否则就不再继续。,关于递归的概念,有些初学者感到不好理解,下面用一个通俗的例子来说明。 例8.7 有5个人坐在一起,问第5个人多少岁?他说

44、比第4个人大2岁。问第4个人岁数,他说比第3个人大2岁。问第3个人,又说比第2个人大2岁。问第2个人,说比第1个人大2岁。最后问第1个人,他说是10岁。请问第5个人多大。显然,这是一个递归问题。要求第5个人的年龄,就必须先知道第4个人的年龄,而第4个人的年龄也不知道,要求第4个人的年龄必须先知道第3个人的年龄,而第3个人的年龄又取决于第2个人的年龄,第2个人的年龄取决于第1个人的年龄。而且每一个人的年龄都比其前1个人的年龄大2。,即 age(5)age(4)2 age(4)age(3)2 age(3)age(2)2 age(2)age(1)2 age(1)10 可以用式子表述如下: age(n

45、)10(n1) age(n1)2 (n1) 可以看到,当n1时,求第n个人的年龄的公式是相同的。因此可以用一个函数表示上述关系。图8.11表示求第5个人年龄的过程。,图8.11,从图可知,求解可分成两个阶段:第一阶段是“回推”,即将第n个人的年龄表示为第(n1)个人年龄的函数,而第(n1)个人的年龄仍然不知道,还要“回推”到第(n2)个人的年龄直到第1个人年龄。此时age(1)已知,不必再向前推了。 然后开始第二阶段,采用递推方法,从第1个人的已知年龄推算出第2个人的年龄(12岁),从第2个人的年龄推算出第3个人的年龄(14岁)一直推算出第5个人的年龄(18岁)为止。也就是说,一个递归的问题可

46、以分为“回推”和“递推”两个阶段。要经历许多步才能求出最后的值。显而易见,如果要求递归过程不是无限制进行下去,必须具有一个结束递归过程的条件。例如,age(1)10,就是使递归结束的条件。,可以用一个函数来描述上述递归过程: age(int n)/*求年龄的递归函数*/ int c; /* c用作存放函数的返回值的变量*/ if(n1) c10; else cage(n1)2; return(c); main() printf(d,age(5); ,运行结果如下: 18 main函数中只有一个语句。整个问题的求解全靠一个age(5)函数调用来解决。函数调用过程如图8.12所示。,图8.12,从

47、图8.12可以看到:age函数共被调用5次,即age(5)、age(4)、age(3)、age(2)、age(1)。其中age(5)是main函数调用的,其余4次是在age函数中调用的,即递归调用4次。请读者仔细分析调用的过程。应当强调说明的是在某一次调用age函数时并不是立即得到age(n)的值,而是一次又一次地进行递归调用,到age(1)时才有确定的值,然后再递推出age(2)、age(3)、age(4)、age(5)。请读者将程序和图8.11、图8.12结合起来认真分析。,例8.8用递归方法求n!。 求n!可以用递推方法,即从1开始,乘2,再乘3一直乘到n。这种方法容易理解,也容易实现。

48、递推法的特点是从一个已知的事实出发,按一定规律推出下一个事实,再从这个新的已知的事实出发,再向下推出一个新的事实这是和递归不同的。 求n!也可以用递归方法,即5!等于4!5,而4!3!41!1。可用下面的递归公式表示: n!1(n0,1) n(n1)!(n1) 有了例8.7的基础,很容易写出本题的程序:,float fac(int n) float f; if(n0) printf(n0,dataerror!);f=-1; else if(n0n1) f1; else ffac(n1)*n; return(f); main() int n; float y;,printf(input a in

49、teger number:); scanf(d,n); yfac(n); printf(d!150f,n,y); 运行情况如下: input a integer number:10 10! 3628800.,例8.9hanoi(汉诺)塔问题。这是一个古典的数学 问题,是一个只有用递归方法(而不可能用其他方法)解决的问题。问题是这样的:古代有一个梵塔,塔内有3个座A、B、C,开始时座上有64个盘子,盘子大小不等,大的在下,小的在上(图8.13)。有一个老和尚想把这64个盘子从座移到座,但每次只允许移动一个盘,且在移动过讨性?个座上都始终保持大盘在下,小盘在上。在移动过程中可以利用座,要求编程序打

50、印出移动的步骤。,图8.13 可以肯定地说:任何一个人(包括“天才”) 都不可能直接写出移动盘子的每一个具体步骤。请读者试验一下按上面的规律将5个盘子从A座移到C座,能否直接写出每一步骤?老和尚自然会这样想:假如有另外一个和尚能有办法将63个盘子从一个座移到另一座。,那么,问题就解决了。此时老和尚只需这样做: (1) 命令第2个和尚将63个盘子从A座移到B座; (2) 自己将1个盘子(最底下的、最大的盘子)从A座移到C座; (3) 再命令第2个和尚将63个盘子从B座移到C座。 至此,全部任务完成了。这就是递归方法。但是,有一个问题实际上未解决:第2个和尚怎样才能将63个盘子从A座移到B座?为了

51、解决将63个盘子从A座移到B座,第2个和尚又想:如果有人能将62个盘子从一个座移到另一座,我就能将63个盘子从A座移到B座,他是这样做的:,(1) 命令第3个和尚将62个盘子从A座移到C座; (2) 自己将1个盘子从A座移到B座; (3) 再命令第3个和尚将62个盘子从C座移到B座。 再进行一次递归。如此“层层下放”, 直到后来找到第63个和尚,让他完成将2个盘子从一个座移到另一座,进行到此,问题就接近解决了。最后找到第64个和尚,让他完成将1个盘子从一个座移到另一座,至此,全部工作都已落实,都是可以执行的。可以看出,递归的结束条件是最后一个和尚只需移一个盘子。否则递归还要继续进行下去。,应当

52、说明,只有第64个和尚的任务完成后,第63个和尚的任务才能完成。只有第2到第64个和尚任务完成后,第1个和尚的任务才能完成。这是一个典型的递归的问题。为使问题简化,我们先分析将座上3个盘子移到座上的过程: (1) 将座上2个盘子移到座上(借助); (2) 将座上1个盘子移到座上; (3) 将座上2个盘子移到座上(借助)。 其中第2步可以直接实现。第1步又可用递归方法分解为:,11将上1个盘子从移到; 12将上1个盘子从移到; 13将上1个盘子从移到。 第3步可以分解为: 31将上1个盘子从移到上; 32将上1个盘子从移到上; 33将上1个盘子从移到上。 将以上综合起来,可得到移动3个盘子的步骤

53、为 ,。,共经历7步。由此可推出:移动n个盘子要经历2n-1步。如移4个盘子经历15步,移5个盘子经历31步,移64个盘子经历264-1步。 由上面的分析可知:将n个盘子从座移到座可以分解为以下3个步骤: (1) 将上n1个盘借助座先移到座上。 (2) 把座上剩下的一个盘移到座上。 (3) 将n1个盘从座借助于座移到座上。 上面第1步和第3步,都是把n1个盘从一个座移到另一个座上,采取的办法是一样的,只是座的名字不同而已。为使之一般化,可以将第1步和第3步表示为:,“将“one” 座上n1个盘移到“two” 座(借助“three” 座)。只是在第步和第步中,one、two、three和、的对应

54、关系不同。对第步,对应关系是one,two,three。对第步,是:one,two,three。 因此,可以把上面3个步骤分成两类操作: (1) 将n1个盘从一个座移到另一个座上(n1)。这就是大和尚让小和尚做的工作,它是一个递归的过程,即和尚将任务层层下放,直到第64个和尚为止。 (2) 将1个盘子从一个座上移到另一座上。这是大和尚自己做的工作。,下面编写程序。分别用两个函数实现以上的两类操作,用hanoi函数实现上面第1类操作(即模拟小和尚的任务),用move函数实现上面第2类操作(模拟大和尚自己移 盘),函数调用hanoi(n,one,two,three)表示“将n个盘子从“one” 座

55、移到“three” 座的过程(借助“two”针”)。函数调用move(x,y)表示将1个盘子从x 座移到y 座的过程。x和y是代表、座之一,根据每次不同情况分别取、代入。,程序如下: void move(char x,char y) printf(ccn,x,y); void hanoi(int n,char one,char two,char three) /*将n个盘从one座借助two座,移到three座*/ if(n1) move(one,three); else, hanoi(n1,one,three,two); move(one,three); hanoi(n1,two,one,t

56、hree); main() int m; printf(input the number of diskes:); scanf(d,m);,printf(The step to moving 3d diskes:n,m); hanoi(m,); 运行情况如下: input the number of diskes:3 The step to moving 3 diskes: , 在本程序中move函数并未真正移动盘子,而只是打印出移盘的方案(从哪一个座移到哪一个座)。 由于篇幅关系,不再对上述程序做过多解释,请读者仔细理解。,8.7 数组作为函数参数 前面已经介绍了可以用变量作函数参数,此外,

57、数组元素也可以作函数实参,其用法与变量相同。数组名也可以作实参和形参,传递的是整个数组。 1. 数组元素作函数实参 由于实参可以是表达式形式,数组元素可以是表达式的组成部分,因此数组元素当然可以作为函数的实参,与用变量作实参一样,是单向传递,即“值传送”方式。 例8.10有两个数组a、b,各有10个元素,将它们对应地逐个相比(即a0与b0比,a1与b1比)。如果a数组中的元素大于b数组,中的相应元素的数目多于b数组中元素大于a数组中相应元素的数目(例如,aibi6次,biai3次,其中i每次为不同的值),则认为a数组大于b数组,并分别统计出两个数组相应元素大于、等于、小于的次数。 程序如下:

58、main() int large(int x,int y); /*函数声明*/ int a10,b10,i,n0,m0,k0; printf(“enter array an”); for(i0;i10;i) scanf(d,ai);,printf(n); printf(enter array bn); for(i0;i10;i) scanf(d,bi); printf(n); for(i0;i10;i) if(large(ai,bi)1)nn1; else if(large(ai,bi)0) mm1; else kk1;,printf(aibi%d timesnai=bi%d timesnai

59、k) printf(array a is larger than array bn); else if (nk) printf(array a is smaller than array bn); else printf(array a is equal to array bn); large(int x,int y) int flag;,if(xy)flag1; else if(xy)flag1; else flag0; return(flag); 运行情况如下: enter array a: 1 3 5 7 9 8 6 4 2 0 enter array b 5 3 8 9-1-3 5 6

60、 0 4,aibi 4 times aibi 1 times aibi 5 times array a is smaller than array b 2. 数组名可作函数参数 可以用数组名作函数参数,此时实参与形参都应用数组名(或用指针变量,见第9章)。 例8.11有一个一维数组score,内放10个学生成绩,求平均成绩。 程序如下:,float average(float array10) int i; float aver,sumarray0; for(i1;i10;i) sumsumarrayi; aversum10; return(aver); main() float score1

61、0,aver; int i;,printf(input 10 scores:n); for(i0;i10;i) scanf(f,scorei); printf(n); averaverage(score); printf(average score is 52f,aver); 运行情况如下: input 10 scores: 10056789857687996757597 average score is 8340,说明: (1) 用数组名作函数参数,应该在主调函数和被调用函数分别定义数组,例中array是形参数组名,score是实参数组名,分别在其所在函数中定义,不能只在一方定义。 (2)

62、实参数组与形参数组类型应一致(今都为float型),如不一致,结果将出错。 (3) 在被调用函数中声明了形参数组的大小为10,但在实际上,指定其大小是不起任何作用的,因为编译对形参数组大小不做检查,只是将实参数组的首地址传给形参数组。因此,scoren和arrayn指的是同一单元。,(4) 形参数组也可以不指定大小,在定义数组时在数组名后面跟一个空的方括弧,为了在被调用函数中处理数组元素的需要,可以另设一个参数,传递数组元素的个数,例8.11可以改写为例8.12形式。 例8.12 float average(float array ,int n) int i; float aver,sumar

63、ray0; for(i1;in;i) sumsumarrayi; aversumn;,return(aver); main() float score-15985,97,915,60,55; float score-210=67.5,89.5,99,69.5,77,89.5,76.5,54,60,99.5; printf(the average of class A is %6.2fn,average(score-1,5); printf(the average of class B is %6.2fn,average(score-2,10); ,运行结果如下: the average of

64、class is 8040 the average of class is 7820 可以看出,两次调用average函数时需要处理的数组元素个数是不同的,在第一次调用时用一个实参5传递给形参n,表示求前面5个学生的平均分。第2次调用时,求10个学生平均分。 (5) 最后应当说明一点: 用数组名作函数实参时,不是把数组的值传递给形参,而是把实参数组的起始地址传递给形参数组,这样两个数组就共占同一段内存单元。见图8.14。假若a的起始地址为1000,则b数组的起始地址也是1000,显然,a和b同占一段内,存单元,a0与b0同占一个单元。由此可以看到,形参数组中各元素的值如发生变化会使实参数组元素

65、的值同时发生变化,从图8.14看是很容易理解的。这一点是与变量做函数参数的情况不相同的,务请注意。在程序设计中可以有意识地利用这一特点改变实参数组元素的值(如排序)。 关于数组名作为函数参数,将在第9章介绍完指针变量后作进一步的说明。 起始地址1000 a0a1a2 a3a4a5a6a7a8a9 2468101214161820,b0b1b2b3b4b5b6b7b8b9,图8.14,例8.13 用选择法对数组中10个整数按由小到大排序。所谓选择法就是先将10个数中最小的数与a0对换;再将a1到a9中最小的数与a1对换每比较一轮,找出一个未经排序的数中最小的一个。共比较9轮。 下面以5个数为例说明选择法的步骤。 a0 a1 a2 a3 a4 3 6 1 9 4 未排序时的情况,1 6 3 9 4 将5个数中最小的数1与a0对换 1 3 6 9 4 将余下的4个数中最小的数3与a1对换 1 3 4 9 6 将余下的3个数中最小的数4与a2对换 1 3 4 6 9 将余下的2个数中最

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

copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!