1.01k likes | 1.14k Views
课件制作:刘达明 023-66834110. 《C 语言程序设计 》 龙昭华主编. 第 5 章 函 数. 第一节 函数的概念 第二节 函数的嵌套调用与递归调用 第三节 指向函数的指针 第四节 变量分类与存储类别 第五节 主函数带参数 第六节 多文件之间的调用. 1 / 101. 课件制作:刘达明 023-66834110. (第五章 函 数). 第一节 函数的概念. 一、 概述.
E N D
课件制作:刘达明 023-66834110 《C语言程序设计》龙昭华主编 第5章函 数 第一节 函数的概念 第二节 函数的嵌套调用与递归调用 第三节 指向函数的指针 第四节 变量分类与存储类别 第五节 主函数带参数 第六节 多文件之间的调用 1 /101
课件制作:刘达明 023-66834110 (第五章 函 数) 第一节函数的概念 一、概述 在程序设计中,对于大型程序设计往往是把复杂的问题分解成许多简单的小问题,通过对小问题的求解来实现对大问题的求解,从而解决大型软件的编程问题。对于小问题(或称小模块)在高级语言程序中,相当于就是一个子程序。而在C语言中,称其为函数。 1、模块化程序设计 基本思想:将一个大的程序按功能分割成一些小模块。 特点:①各模块相对独立、功能单一、结构清晰、接口简单。②控制了程序设计的复杂性。 ③提高元件的可靠性。 ④缩短开发周期。 ⑤避免程序开发的重复劳动。 ⑥易于维护和功能扩充。 开发方法:自上向下,逐步分解,分而治之。 一个小模块就是一个函数,在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。要善于利用函数,以减少重复编写程序段的工作量。在编写某个函数时,遇到具有相对独立的功能的程序段,都应独立成另一个函数,而在一个函数中调用另一个函数;当某一个函数拥有较多的代码时(一般函数代码50行左右),也应将函数中相对对立的代码分成另一个函数。 2 /101
C程序结构 课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 2、C是模块化程序设计语言 ①C是函数式语言。 ②C程序中必须有且只能有一个名为main的主函数。 ③C程序的执行总是从main 函数开始,在main中结束。 ④函数不能嵌套定义,可以 嵌套调用。 ⑤一个源程序文件由一个或 多个函数组成。一个源程文 件是一个编译单位,而不是 函数为编译单位。 ⑥一个C程序由一个或多个源程序文件组成。一个源文件可以为多个C程序公用。 3 /101
主函数 main( ) 函数 f1( ) 函数 f2( ) 函数h2() 函数h1() 函数h3() 课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 3、函数的分类 ⑦所有函数都是平行的,可以互相调用,但不能调用main函数。 ⑧源程序文件名为:xxxxxxxx.c的形式。一个源程序文件包括预编译命令和若干函数。但一个C程序中的所有源程序中只能包含一个main函数。 在C语言中,从用户的使用角度来看,函数主要分为两种: ①标准库函数。C语言为了程序设计的方便,把一些常用的功能和算法,定义成单一功能模块的函数,它是由C语言预先编写的一系列常用函数,如strcat,strcpy,strcmp、strlen等。在Turbo C 2.0中提供的运行程 序库中有400多个函数,其中包括对PC机低端控制、DOS的接口、输入/输出、过程管理、算术操作等。在使用这些库函数时,只需在程序前包含有该函数原型的头文件即可。库函数是由系统提供的。 4 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) ②用户自定义函数。 由用户根据自己功能的需要编制的函数。在自定义函数中,可以编制面向某一特定应用领域目的的功能,可形成自定义函数库,在需要时调用它。用户自定义函数时,应考虑定义、说明、调用三方面的内容。 参见教材P136的例5.2,两个字符串的比较。 使用库函数printf和自定义函数s_cmp(char x,char y)。 使用库函数时,必须使用:#include <stdio.h> 包含头文件。 自定义函数必须在使用之前声明:char s_cmp(char x,char y); 在C语言中,从函数的形式来看,函数又分为两类: ①无参函数。在调用无参函数时,主调函数并不将数据传送给被调用函数,一般用来执行指定的一组操作。无参函数可以带回或不带回函数值,单一般以不带回函数值的居多。 ②有参函数。在调用函数时,在主调函数和被调用函数之间有数据传递。 5 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 二、函数的定义 如:printstar() { printf(“*************\n”); } 1、无参函数的定义形式 类型说明符 函数名() { 声明部分 语句体 } 2、有参函数的定义形式 类型说明符 函数名(形式参数表) { 声明部分 语句体 } 3、可以有“空函数” 类型说明符 函数名() { } 如:int max (int x,int y) { int z; z=x>y ? x : y; return(z); } 如:int min () { } 一对花括号内的声明部分和语句体组成了函数的函数体。 6 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 说明: ①类型说明符可为:int、char、float、double等。表示在调用了该函数后,其返回值的数据类型;如果函数无数据返回时,应使用void作类型定义符。注意,省略了类型说明符,C语言编译程序认为函数返回值为一个整型值类型(int)。 ②函数名即函数的名称,是由用户取的合法标识符。C语言的关键字不能作函数名。自定义函数的名称可以使用库函数名,但这时库函数被屏蔽。 ③形式参数表是一个用逗号分隔的变量表,当函数被调用时这些变量接受调用参数的值。相当于函数调用时传递信息的通道。 ④带有形式参数的函数一般定义为(称之为现代方式): 类型说明符 函数名(类型 参数1,类型 参数2,…,类型 参数n) { 声明部分; 语句体; } 7 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 说明(续): 而传统的带形式参数的函数的定义方式则为(称之为传统方式): 类型说明符 函数名(参数1, 参数2,…, 参数n) 形参类型说明; { 声明部分; 语句体; } 如:flaot add(float x,float y) 或 float add(x,y) { float x,y; x=x+y; { x=x+y; return(x); return(x); } } ⑤在函数的定义中,如果没有函数体,即函数什么功能都不做,我们称为空函数。空函数的功能主要是在程序设计中,留出该函数的功能,以后在需要的时候补充上去。 8 /101
(主调函数) c=max(a , b); (被调函数) max(int x, int y) 参数传递 实际参数 形式参数 课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 三、函数参数与函数的值 1、函数参数 C语言程序是由若干个函数组成的,各函数在结构上是独立的,但它们所处理的对象即数据却是相互联系的。一个函数定义好后,就必须用一定的方式去调用它。一个函数调用另一个函数,将调用的函数称为主调函数,将被调用的函数称为被调函数。主调函数和被调函数具有数据传递的关系,其传递的实施是通过函数的参数来实现的。 函数的参数分为形参和实参两种。形参(即形式参数)出现在函数定义中;实参(即实际参数)出现在主调函数中。函数调用时,主调函数把实参的值传送给被调函数的形参,从而实现主调函数向被调函数的数据传送。 9 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) ⑴关于形参与实参的说明 ①在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元。形参变量只有在被调用时才分配内存单元,在调用结束后,立即释放所分配的内存单元因此,形参只在函数内部才有效。 ②实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值(或地址),以便把这些值传送给形参。 ③实参和形参在数量上、类型上、顺序上应严格一致,否则会发生“类型不匹配”的错误。对实参与形参数据类型不同时,应在带入实参时做相应的数据类型转换。而对于字符型与整型在有效的数据范围内可以互相通用。 ④C语言规定,实参变量对形参变量的数据传递是“值传递”,即单向传递。只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。在内存中,实参单元与形参单元是不同的单元。 ⑤在调用函数时,给形参分配存储单元,并将实参对应的值传递给形参。调用结束后形参单元被释放,实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会影响主调函数的实参的值。 10 /101
实参内存空间 形参内存空间 复制 10 10 形参x 实参a 实参内存空间 形参内存空间 形参x 实参a 课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) ⑵参数传递的两种方式 ①数值传递方式 特点:实参通过复制的方式传递给形参(实参和形参占用不同的内存空间)。 数据复制方式就是把数据本身作为实参传递给形参,在传递时,有一个复制的动作,即将实际参数的值复制给形式参数。数值传递方式,如果形参的值发生改变,并不会影响实参的值。 ②地址传递方式 特点:参数传递不是数据本身,而是数据的地址(实参和形参使用同一地址空间)。 11 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 参数传递的例子: 地址传送方式是将数据的存储地址作为参数传递给形式参数,即要求形式参数的数据类型应是指针,对于实际参数来说其数据类型也要求是一个指针。指针类型存储的不是一个具体的数据,而是一个数据的地址。 在参数的传递中,实际上是把实参的地址传送给形参,两个参数共有同一个地址,指向同一个内存单元。所以在被调函数中可以直接对这个内存空间进行操作。当退出被调函数时,虽然形式参数立即随之消失,但其所作的各种操作已经影响了主调函数实际参数所指向存储空间的内容。 在教材P139的例5.3中,使用的是按值传递方式。在被调函数中改变了形参x和y的值,但在主调函数中的实参a和b仍为原值。 在教材P140的例5.4中,使用的是按址传递方式。在被调函数中改变了形参x和y的值,在主调函数中的实参a和b也跟着改变了原值。 注意:由于按地址传递方式可以改变实参的值,因此,可以通过按地址传递方式来返回参数值。但同时要注意,必须确信实参值是要改变的,否则不能使用这种方式。 12 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 2、函数的值 函数的值实际上就是函数的返回值。 在C程序中,一般希望通过函数调用使主调函数从被调函数中得到一个确定的值,这个值就是函数的返回值。该返回值可通过在被调函数定义中用return语句来实现。 ⑴return返回语句的使用 return语句的一般形式: return(表达式); 或 return 表达式; 或 return; 功能:使程序控制从被调用函数返回到主调用函数中,同时把返回值带给调用函数。 13 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 说明: ①如果主调函数需要从被调函数带回一个函数值,被调用函数可用return语句返回其值。如果不需要从被调用函数带回函数值,可以不用return语句。 ②在被调函数中允许有多个return语句,但每次调用只能有一个return语句被执行,因此,只能返回一个值。 ③函数返回值的类型和函数定义中函数的类型应保持一致,若两者类型不一致,则以函数类型为准,自动进行类型转换。 ④为了在函数定义中,明确表示该函数“不返回值”,应把该函数的类型用“void”进行定义(表示“无类型”或“空类型”)。 ⑤如果被调用函数中没有return语句,并不带回一个确定的、用户所希望得到的函数值。实际上,函数并不是不带回值,而只是不带回一个有用的值,带回的是一个不确定的值。 14 /101
例5.2无返回值函数 printstar() { printf("******"); } main() { int a; a=printstar(); printf("%d",a); } void printstar() { printf("******"); } main() { int a; a=printstar(); printf("%d",a); } void swap(int x,int y ) { int temp; temp=x; x=y; y=temp; } main() { int a=10,b=20; swap(a,b); } 输出:10 编译错误! 课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 例5.1函数带回不确定值 15 /101
可改 写成: int max(x,y) float x,y; { int z; z=x>y?x:y; return(z); } main() { float a=10.2,b=20.5; int c; c=max(a,b); } 课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 例5.3函数返回值类型转换 #include <stdio.h> main() { float a,b; int c; int max(float x,float y); /* 被调函数的声明 */ scanf("%f,%f",&a,&b); c=max(a,b); printf("Max is %d\n",c); } max(float x, float y) { float z; z=x>y?x:y; return(z); } max函数中的z自动转换为int型,使函数的返回值的类型为缺省的int类型。 16 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) ⑵函数返回值的地址传递形式 在C程序设计中,函数的设计是为了函数的功能能得到重复的使用。一个函数可以带回一个整型值、字符值、实型值等数据,也可带回指针型的数据,即地址。 也就是说,函数定义中使用return语句一般只能把一个数值返回给主调函数,如果希望要求返回的值多于一个以上时,单独使用return语句是不行的。在这种情况下,可采用地址传递方式或全局变量方式来实现。其返回值的方式和操作是一样的,都用return语句,但返回的值不是数值而是地址。 函数返回值的地址传递方式,实际上是使用指针作为函数的返回值进行数据传递,也就是指针型函数。 返回指针值的函数的一般定义形式为: 类型说明符 *函数名(参数表) 17 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 说明: 定义:int *fun(int x,int y); 其中,fun是函数名,调用它以后能得到一个指向整型数据的指针(地址)。x和y是函数fun的形参为整型。 注意*号两侧没有括弧,在fun的两侧分别为*和()运算符。而()优先级高于*,因此fun先与()结合,显然fun()是函数形式。但这个函数前面有一个*,表示此函数是指针型函数(函数的值是指针)。最前面的int表示返回的指针指向整型变量。 例5.4参见教材P142的例5.6。 定义了一个指针型函数:float *search(float (*pointer)[4],n) 参数为一个指向4个元素的一维数组指针(即行指针),函数返回的pt表示二维数组score某一行的首地址。可以根据pt读取该行的各个元素的值。 18 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 例5.5在字符串中寻找关键字 在字符串中寻找关键字,若找到,给出关键字在字符串中第1次出现的位置。字符串和关键字均由键盘输入。 main() { char c,*string=“I am a student”; int p; printf(“Please enter the character:”); scanf(“%c”,&c); if (search(string,&c)) { p=search(string,&c)-string; printf(“Found!!!,%d\n”,p); else printf(“Not Found!!!\n”); } #include <stdio.h> char *search(char *tagstr,char *c) { char *p=tagstr; while(*p!=‘\0’) { if(*p==*c) return p; p++; } return NULL; } 第一次运行结果为: Please enter the character: b (加回车) Not Found!!! 第二次运行结果为: Please enter the character: a (加回车) Found!!!,2 19 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 四、函数的声明与调用 1、函数的声明 在使用函数之前,必须对函数进行声明。并且被调用的函数必须是已经存在的函数(可以是库函数,也可以是用户自定义函数)。 ⑴库函数的声明 如果使用库函数,应该在本文件的开头用#include命令将调用有关库函数时所需用到的信息“包含”到本文件中来。如#include <stdio.h> #include “string.h”等。 ⑵自定义函数的声明 如果使用用户自己定义的函数,而且该函数与主调函数在同一文件中,还应该在主调函数中对被调函数作声明,即向编译系统声明将要调用此函数,并将有关信息通知编译系统。 20 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 例如: #include <stdio.h> main() { float add(float x,float y); /* 声明add函数 */ float a=10.4,b=20.6,c; c=add(a,b); printf(“sum is %f\n”,c); } /* 定义add函数 */ float add(float x,float y) { float z; z=x+y; return(z); } 运行结果为: sum is 31.000000 函数声明的说明: ①对函数的“定义”和“声明”的含义是不一样的。“定义”是指对函数功能的确定,包括指定函数名、函数值类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。而声明的作用则是把函数名字、函数类型以及形参的类型、个数和顺序通知编译系统。 21 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 函数声明的说明: ②将函数“定义”的第一行加一个分号就成了对函数的“声明”。如: float add(float x,float y)是函数的定义,而float add(float x,float y);就是函数的声明。 事实上,在函数声明中可以不写形参名,只写形参的数据类型。如: float add(float,float); C在编译时只检查函数声明中形参的类型,而不检查形参名。因此在函数声明时,形参名可以不要,也可以与定义函数中的形参名不一致。如: float add(float a,float b); ③在C中的函数声明也称为函数原型,它的作用主要是利用它在程序的编译阶段对调用函数的合法性进行全面检查。 函数原型(即函数声明)的一般形式为: 函数类型 函数名();/* 这是无参数情况的函数声明 */ 函数类型 函数名(参数类型1,参数类型2,…); 或 函数类型 函数名(参数类型1 参数名1,参数类型2 参数名2,…); 22 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 函数声明的说明(续): 注意:应当保证函数声明与函数定义中的首部写法上一致,即函数类型、函数名、参数个数、参数类型和参数顺序必须相同。函数调用时函数名、实参个数应与函数原型一致。实参类型必须与函数原型中的形参类型赋值兼容。 ④在以前的C版本中,函数声明方式只有函数名和函数类型:如: float add();新版本也兼容这种方式,但不提倡这种方式。 ⑤以下几种情况可以在调用函数时不做声明: 函数的数据类型是整型或字符型时,可以不对函数进行说明,直接使用。但这种方式由于系统没有对参数类型做检查,所以即使有错,系统也不报错。因此建议对任何需要调用的函数都要进行声明。对于缺省的函数声明,系统自动将函数值默认为int型。 如果被调用函数的定义在函数调用之前,可不对函数进行声明,直接使用。 无返回值的函数,在调用前也可以不作说明。 如果已在所有函数定义之前,在函数的外部都已做了函数声明,则在各个主调函数中不必对所调用的函数再做声明。 23 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 2、函数调用形式 函数调用的一般形式是: 函数名(实际参数表); 说明: ①如果调用的是无参函数,则“实际参数表”可以没有,但括号()不能省略。 ②如果实参表包含多个实参,则各参数间用逗号隔开。 ③实参与形参的个数应相等、类型应一致。 ④实参与形参按顺序对应,一一传递数据。 特别说明:如果实参表包括多个实参,对实参求值的顺序并不是确定的,有的系统自左至右,有的则是自右至左。TurboC和MSC则是按自右而左的顺序求值的。如:int i=2,p;p=add(i,++i); ⑤函数不能嵌套定义,即在一个定义好的函数中,又定义另一个函数。但是函数之间允许相互调用,也允许嵌套调用。 函数还可以自己调用自己,称为递归调用。 24 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 3、函数调用的方式 函数调用以在程序中出现的位置来分,主要有下列三种函数调用方式: ①函数语句 把函数调用作为一个语句,即在程序中是一个独立的语句行。这时不要求函数带回值只要求完成一定的操作,函数一般定义为void类型。如:printstar(); ②函数表达式 函数出现在一个表达式中,这种表达式称为函数表达式。这时要求函数带回一个确定的值以参加表达式的运算。如:a=5*add(5,6);它是将add返回的值乘以5再赋给变量a。 ③函数参数 有返回值的函数,可将其函数调用,作为另一个函数的实参。如: a=max(10,add(5,6));printf(“%d\n”,add(5,6)); 函数调用作为函数的参数,实质上也是函数表达式形式调用的一种,因为函数的参数本来就要求是表达式形式。 25 /101
课件制作:刘达明 023-66834110 第一节函数的概念 (第五章 函 数) 例5.5判断是否为素数。 (参见教材P144的例5.7) #include <stdio.h> #include <math.h> int i_prime(int); void main(void) { int x; scanf(“%d”,&x); if(i_prime(x)) printf(“%d is prime.\n”); else printf(“%d is not prime.\n”); } /* 素数返回1,非素数返回0 */ int i_prime(int n) { int i; for(i=2;i<=sqrt(n);i++) if(n%i==0) return(0); return(1); } 例5.6大写字母转换为小写字母。 (参见教材P145的例5.8) 26 /101
课件制作:刘达明 023-66834110 《C语言程序设计》龙昭华主编 第5章函数----习题1 习 题 (一) *** 复习本章已讲内容,预习本章剩余内容。 5.1 教材P164~165的习题5.1选择题中的第⑴ 至 ⑺小题。 5.2 教材P169的习题5.4编程题中的⑴小题。 27 /101
main( ) a函数 b函数 调用函数a 调用函数b 结束 课件制作:刘达明 023-66834110 (第五章 函 数) 第二节函数的嵌套调用与递归调用 一、函数的嵌套调用 C语言的函数定义都是互相平行、独立的,也就是说在定义函数时,一个函数内不能包含另一个函数。C语言不能嵌套定义函数,但可以嵌套调用函数,也就是说,在调用一个函数的过程中,又可以调用另一个函数。函数的嵌套调用形式为: 28 /101
main( ) dif函数 max函数 调用函数dif 调用函数max min函数 调用函数min 输出 结束 课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.7 #include <stdio.h> int dif(int x,int y,int z); int max(int x,int y,int z); int min(int x,int y,int z); void main() { int a,b,c,d; scanf("%d%d%d",&a,&b,&c); d=dif(a,b,c); printf("Max-Min=%d\n",d); } /* 定义dif函数求差值 */ int dif(int x,int y,int z) { return max(x,y,z)-min(x,y,z); } /* 定义max函数求三数的最大值 */ int max(int x,int y,int z) { int r; r=x>y?x:y; return(r>z?r:z); } /* 定义min函数求三数的最小值 */ int min(int x,int y,int z) { int r; r=x<y?x:y; return(r<z?r:z); } 求三个数中 最大数和最 小数的差值 29 /101
y f(x2) x1 x 0 x2 x f(x) f(x1) 课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.8用弦截法求方程的根 解:弦截法求方程的根的方法如下: ①取两个不同点x1、x2,如果f(x1)与 f(x2)符号相反,则(x1,x2)区间内必有一个根。否则改变x1、x2的值使得f(x1)与f(x2)异号为止。(注意:x1与x2的值不能相差太大,确保之间只有一个根) ②连接f(x1)与f(x2)两点,此线(即弦) 交x轴于x点(计算见图)。再求出f(x)。 ③若f(x)与f(x1)同号,则将x作为新的x1。若f(x)与f(x2)同号,则将x作为新的x2。 ④重复第②与第③步,直到| f(x)|<, 其中是一个很小的正数,如0.0001, 此时可以认为: f(x)≈0。 30 /101
输入x1,x2,求f(x1),f(x2) 直到f(x1)与f(x2)异号 求f(x1)与f(x2)连线与x轴的交点x y=f(x),y1=f(x1) y与y1同号 假 真 root函数 x2=x x1=x y1=y 直到|y|< root=x 输出root 课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.8 N-S图 #include <math.h> /* 定义f函数,求f(x)的值 */ float f(float x) { float y; y=((x-5.0)*x+16.0)*x-80.0; return(y); } /* 定义xpoint函数,求弦与x轴的交点 */ float xpoint(float x,float y) { float y; y=(x1*f(x2)-x2*f(x1))/(f(x2)-f(x1)); return(y); } 31 /101
课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.8程序 /* 定义root函数,求近似根 */ float root(float x1,float x2) { float x,y,y1; y1=f(x1); do { x=xpoint(x1,x2); y=f(x); if (y*y1>0) { y1=y; x1=x; }else x2=x; }while(fabs(y)>=0.0001); return(x); } /* 主函数 */ main() { float x1,x2,f1,f2,x; do { printf("Input x1,x2:\n"); scanf("%f,%f",&x1,&x2); f1=f(x1); f2=f(x2); }while(f1*f2>=0); x=root(x1,x2); printf("A root of equation is %8.4f",x); } 运行结果为: Input x1,x2:2,6 (加回车) A root of equation is 5.0000 32 /101
f函数 main( ) xpoint函数 root函数 调用函数root 调用函数xpoint 调用函数f 输出根 x 结束 课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.8函数调用关系 33 /101
用迭代法求 。求平方根的迭代公式为: 课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.9 解:①可详见第三章结构化程序设计教案的例3.38。循环迭代方法求解。 ②参见教材P153的例5.16。该例使用函数嵌套调用方法求解。 #include <stdio.h> main() { float squ_root(float x); float x,s; do { scanf(“%f”,&x); }while(x<=0); s=squ_root(x); printf(“x=%f,s=%f”,x,s); } /* 求x的平方根 */ float squ_root(float x) { float absv(float x); float g=1.0,e=1e-5; while(absv(g*g-x)>e) g=(x/g+g)/2.0; return(g); } /* 求x的绝对值 */ float absv(float x) { if(x<0) x=-x; return(x); } 34 /101
u>v T F 交换u和v,使大者为被除数v 除数a=u,被除数b=v b/a的余数不为0 使除数变为被除数,余数变为除数 返回最大公约数a 课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.10求最大公约数和最小公倍数 解:设两个整数为u和v,则求最大公约 数的算法见右图。而最小公倍数=u*v/最 大公约数。 /* 定义hcf函数求最大公约数 */ int hcf(int u,int v) { int a,b,t,r; if (u>v) { t=u; u=v; v=t; } a=u; b=v; while ((r=b%a)!=0) { b=a; a=r; } return(a); } /* 定义lcd函数求最小公倍数 */ int lcd(int u,int v,int h) { return(u*v/h); } main() /* 主函数 */ { int u,v,h,l; scanf(“%d,%d”,&u,&v); h=hcf(u,v); printf(“H.C.F=%d\n”,h); l=lcd(u,v,h); printf(“L.C.D=%d\n”,l); } 运行结果为: 24,16 (加回车) H.C.F=8 L.C.D=48 35 /101
int f2(int t) { int a,c; …… c=f1(a); ……. return(3+c); } int f1(int x) { int y,z; …… z=f2(y); ……. return(2*z); } f( ) f1( ) f2( ) 调f 调f2 调f1 课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 二、函数的递归调用 int f(int x) { int y,z; …… z=f(y); ……. return(2*z); } 在函数调用中,如果直接或间接地调用该函数本身,称为递归调用。递归有时也称为循环定义。 递归又分为:直接递归调用,即函数直接调用自身。和间接调用,即函数互相调用对方。 36 /101
课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 说明: ①从图中看到,这两种递归调用都是无终止的自身调用。程序中不应出现无终止调用而只应出现有限次数的有终止的递归调用。可以使用if语句来控制。 ②在递归程序中:由递归方式与递归终止条件两部分组成。即一个递归的问题可以分为:首先“回推”,然后“递推”。在递归过程中,必须具有一个结束递归过程的条件。 ③C编译系统对递归函数的自调用次数没有限制。 ④每调用函数一次,在内存堆栈区分配空间,用于存放函数变量、返回值等信息,所以递归次数过多,可能引起堆栈溢出。 37 /101
课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.11用递归方法求n! 解:⑴求n!可以用递推方法,即从1开始,乘2,再乘3,…,一直乘到n。递推法的特点是从一个已知的事实出发,再向下推出一个新的事实。这与递归方法是不同的。⑵求n!也可以用递归方法,即:n!=n*(n-1)!,(n-1)!=(n-1)*(n-2)!,…,3!=3*2!,2!=2*1!。 n!的递归公式为: 对使用递归方法计算4!的结果如下: 调用 递归调用 返回 第1步 fac(4) fa=4*fac(4-1)=4*fac(3) fa=4*3*2*1 第2步 fac(3) fa=3*fac(3-1)=3*fac(2) fa=3*2*1 第3步 fac(2) fa=2*fac(2-1)=2*fac(1) fa=2*1 第4步 fac(1) fa=fac(1)=1 回推过程:计算fac(4)时要知道fac(3),计算fac(3)时要知道 fac(2),计算fac(2)时要知道 fac(1),而fac(1)为1。递推过 程:知道fac(1)后可算fac(2),从而可算fac(3),最后计算fac(4)。 递推 回推 38 /101
课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.11(续) #include <stdio.h> long fac(int n); /* 函数声明 */ main() /* 主函数 */ { int n; long s; printf(“Input n:”); scanf(“%d”,&n); s=fac(n); /* 调用自定义函数 */ printf(“%d!=%ld”,n,s); } /* 定义fac函数求n!的值 */ long fac(int n) { long fa; if (n==0 || n==1) /* 终止条件 */ fa=1; else fa=n*fac(n-1); /* 自己递归调用 */ return(fa); } 运行结果为: Input n:10 (加回车) 10!=3628800 39 /101
n (n=0,1) Fib(n)= Fib(n-1)+Fib(n-2) (n>1) 课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.12求解斐波那契数列的递归算法。 解:Fibonacci数列在第三章例3.27和第四章例4.3中都有讲解。(详见教材P156例5.18) #include<stdio.h> main ( ) { long Fib ( int n ); /* 函数声明 */ long s; int i ; scanf(“%d”,&i); s=Fib(i); printf(“Fib(%d)=%ld\n”,i,s ) ; } /* 定义Fib函数求数列的第n项 */ long Fib ( int n ) { if ( n <= 1 ) /* 终止条件 */ return n; else return Fib (n-1) + Fib (n-2); } 运行结果为: 6 (加回车) Fib(6)=8 运行结果为: 4 (加回车) Fib(4)=3 说明:每次递归调用fib函数时,都要调用该函 数两次。即计算n个fibonacci数列的值,递归调 用的次数为2n。计算第20项的fibonacci数需要 递归调用的次数为220 (大约100万次),计算第4 项则只需要16次。这种现象称为指数复杂度。 虽然程序的算法简单,但计算的复杂度会随着 n的增加而呈指数级增长。 40 /101
课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.13Hanoi(汉诺)塔问题 这是一个古典的数学问题,是一个只有用递归方法解决的 问题。问题是这样的:在古代有一个梵塔,塔内有3个座A、 B、C,开始时A座上有64个盘子,大小不等,大的在下,小的 在上,现在需把A座上的盘子移到C座上,条件是一次只能移 动一个盘子,而且不允许大盘子放在小盘子上面,在移动的 过程中,可以借助B座进行移动。如右图所示。(参见P156) void move(char x,char y) { printf(“%c- ->%c\n”,x,y); } void hanoi(int n,char one,char two,char three) { if (n==1) move(one,three); else { hanoi(n-1,one,three,two); move(one,three); hanoi(n-1,two,one,three); } } /* 将n个盘从one座借助two座移到three座 */ main() { int m; printf(“Input m:”); scanf(“%d”,&m); printf(“The step to moving %3d diskes:\n”,m); hanoi(m,’A’,’B’,’C’); } 当m=3时,A->C,A->B,C->B, A->C,B->A,B->C,A->C 41 /101
输入整数number number<0 T F 输出负号 使number变为正数 递归调用convert函数输出字符 课件制作:刘达明 023-66834110 第二节函数的嵌套调用与递归调用 (第五章 函 数) 例5.14用递归法将整数n转换成字符串 解:如n=483时,输出的字符串为“483”。 #include <stdio.h> main() { void convert(int n); int number; printf(“Input number:”); scanf(“%d”,&number); printf(“\nOutput is:”); if (number<0) { putchar(‘-’); number=-number; } convert(number); } void convert(int n) { int i; if ((i=n/10)!=0) convert(i); putchar(n%10+’0’); } 运行结果为: Input number:2345 (加回车) Output is:2345 42 /101
课件制作:刘达明 023-66834110 (第五章 函 数) 第三节指向函数的指针 一、指向函数的指针 定义一个函数,其函数名是函数调用的主要标识。 在C语言中,函数名与数组名具有相同的地址性质,它表示是被定义函数的存储首地址,即函数调用后函数执行的入口地址。由于函数名是函数的入口地址,而指针描述的是地址。因此,可以将一个函数名赋给一个指针变量,然后通过这个指针变量对其函数进行操作。 一个函数在编译时,自动分配一个入口地址,这个入口地址称为函数的指针。 1、函数指针变量的定义 定义函数指针变量的一般形式为: 类型标识符 (*指针变量名)(); 例如:int (*p)(); 43 /101
课件制作:刘达明 023-66834110 第三节指向函数的指针 (第五章 函 数) 说明: ①类型标识符int,是指函数返回值的类型,即返回值为整型。 ②(*p)()表示定义了一个指向函数的指针变量p,其专门用来存放函数的入口地址的。 ③函数指针与普通指针一样,可以对多个函数进行操作。即在一个程序应用中,一个函数指针变量可以先后指向不同的函数。但是,函数的类型应以函数指针的类型一样。 ④该指针变量p,只能用于指向函数的入口地址,不能用于指向一般变量或数组。函数指针在程序的使用中,把哪一个函数的入口地址赋给它,它就指向哪一个函数。 ⑤对指向函数的指针变量,像p+n、p++、p—等运算都是无意义,这是与普通指针变量的区别。 44 /101
课件制作:刘达明 023-66834110 第三节指向函数的指针 (第五章 函 数) 2、函数指针变量的使用 例5.15求a和b中的最大者。 #include <stdio.h> main() { int max(int,int); /* 声明函数 */ int (*p)(); /* 定义函数指针变量 */ int a,b,c; p=max; /* 函数指针引用函数 */ scanf(“%d,%d”,&a,&b); c=(*p)(a,b); printf(“a=%d,b=%d,c=%d\n”,a,b,c); } /* 定义max函数求两数的最大值 */ int max(int x,int y) { return(x>y?x:y);} #include <stdio.h> main() { int max(int,int); /* 声明函数 */ int a,b,c; scanf(“%d,%d”,&a,&b); c=max(a,b); printf(“a=%d,b=%d,c=%d\n”,a,b,c); } /* 定义max函数求两数的最大值 */ int max(int x,int y) { return(x>y?x:y);} main中有关max函数的使用可以改成函 数指针形式来引用max函数。如右框: 45 /101
max p 指令1 指令2 … 课件制作:刘达明 023-66834110 第三节指向函数的指针 (第五章 函 数) 函数指针变量的使用说明: ①函数指针变量的赋值:如:p=max;其中:max为函数名,p为函数指针。由函数名进行直接赋值,但不能给出函数参数。 ②用函数指针变量调用函数:如:c=(*p)(a,b);如果函数有参数,在用函数指针调用函数时,必须给出参数;如果函数没有参数,在调用函数时,不须给出参数。 ③既然p和max都是函数max的入口地址,因此(*p)(a,b)和(*max)(a,b)都表示max(a,b)函数。也就是说:c=(*p)(a,b);、c=(*max)(a,b);、c=max(a,b);将得到相同的值,但这三者的含义是不同的。当然,由于c=max(a,b);本身就是函数形式,因此一般来讲没有写成c=(*max)(a,b);的习惯。 ④在c=(*p)(a,b);中,由于函数指针变量p指向的函数返回值是整型,而c也是整型,因此这种赋值方式是合法的。 46 /101
课件制作:刘达明 023-66834110 第三节指向函数的指针 (第五章 函 数) 3、函数指针变量作函数参数 函数指针变量作函数参数,实际上是函数的一种灵活的使用方式,它可实现函数地址的传递,即将函数名传给形参。 假定有一个函数sub,它有两个形参x1和x2,定义x1和x2为指向函数的指针变量。在调用函数sub时,实参用两个函数名f1和f2给形参传递函数地址。这样在函数sub中就可以调用f1和f2函数了。如下列程序(运行结果为a=10): int f2(int x,int y) /* 定义f2函数 */ { return(x*y); } /* 定义x1、x2为函数指针变量,x1指 向的函数有一个整型形参, x2指向 的函数有两个整型形参 */ int sub(int (*x1)(int),int (*x2)(int,int)) { int a,b,i=2,j=3; a=(*x1)(i); b=(*x2)(i,j); return(a+b); } main() { int f1(int); /*函数声明 */ int f2(int,int); int sub(int (*x1)(int),int (*x2)(int,int)); int a; clrscr(); a=sub(f1,f2); printf(“a=%d\n”,a); } int f1(int x) /* 定义f1函数 */ { return(2*x); } 47 /101
课件制作:刘达明 023-66834110 第三节指向函数的指针 (第五章 函 数) 例5.16求两数的最大值、最小值、两数之和 解:详见教材P147例5.10。 sub( int x,int y,int (*fun)(int,int) ) { int s; s=(*fun)(x,y); printf(“%d\n”,s); } int max(int x,int y) { return(x>y?x:y); } int min(int x,int y) { return(x<y?x:y); } int add(int x,int y) { return(x+y); } #include <stdio.h> main() { int max(int,int),min(int,int),add(int,int); int a,b; scanf(“%d,%d”,&a,&b); printf(“max=“); sub(a,b,max); printf(“min=“); sub(a,b,min); printf(“sum=“); sub(a,b,add); } 48 /101
fun max 函数 fun min 函数 fun add 函数 课件制作:刘达明 023-66834110 第三节指向函数的指针 (第五章 函 数) 说明: 从本例可以看出,在main函数中第一次调用sub函数时,除了将a和b作为实参将两个数传给sub的形参x和y外,还将函数名max作为实参将其入口地址传送给sub函数中的形参fun。这时sub函数中的(*fun)(x,y)相当于max(x,y),执行sub可以输出a和b中的大者。 可以看到,不论调用max、min、add,函数sub都没有改动,只是在调用sub时实参改变而已。这就增加了函数使用的灵活性,可以编一个通用的函数来实现各种专用的功能。 49 /101
课件制作:刘达明 023-66834110 第三节指向函数的指针 (第五章 函 数) 说明(续): 如求积分的值: 可以定义float integral(float a,float b,float (*fun)(float))函数来求不同的定积分,形参a,b为上下限,fun为指向定积分函数的指针变量。可以分别定义5个C函数f1,f2,f3,f4,f5用来求定积分函数的值。 特别注意:对作为实参的函数一定要在主调函数或外部进行声明。它与前面讲过的缺省声明为int不同,因为函数名作为实参系统不知道它是函数,需要在前面声明。 50 /101