2.14k likes | 2.32k Views
C 程 序 设 计. 江南大学控制科学与工程研究中心 张正道 (wxzzd@hotmail.com). 第 8 章 函数. 概述 函数定义的一般形式 函数参数和函数的值 函数的调用(嵌套调用、递归调用) 数组作为函数参数 局部变量和全局变量 动态存储变量和静态存储变量 内部函数和外部函数. §8.1 概 述. 一 . 模块与函数. 1. 结构化程序设计. ◆ 一种设计程序的技术。. ◆ 通常采用自顶向下逐步求精的设计方法和单入 单出控制结构。. ◆ C 语言是结构化的程序设计语言。.
E N D
C 程 序 设 计 江南大学控制科学与工程研究中心 张正道(wxzzd@hotmail.com)
第8章 函数 概述 函数定义的一般形式 函数参数和函数的值 函数的调用(嵌套调用、递归调用) 数组作为函数参数 局部变量和全局变量 动态存储变量和静态存储变量 内部函数和外部函数
§8.1 概 述 一. 模块与函数 1. 结构化程序设计 ◆一种设计程序的技术。 ◆通常采用自顶向下逐步求精的设计方法和单入 单出控制结构。 ◆ C语言是结构化的程序设计语言。 2.“自顶向下”的模块化程序设计方法 将一个大问题按层次分解成多个方便解决小问题的模块的设计思想.
主控模块 模块1 模块2 模块n 模块n_n 模块1_1 模块1_n 模块2_1 模块2_n 模块n_1 3. 功能模块 求解较小问题的算法和程序称作“功能模块”, 各功能模块可以先单独设计,然后将求解所有子问题的模块组合成求解原问题的程序。 由功能模块组成程序的结构图
4. 函数: 完成相对独立功能的程序 [例] 输入年月日,计算出该日为该年的第几天。 分析: (1)判断年份是否为闰年。闰年的二月为29天,平年的二月为28天。因此,给定一个年份,首先应确定其是否是闰年。 (2)求月份的天数。1、3、5、7、8、10、12月每月31天,4、6、9、11月每月30天,2月根据所在年份是否为闰年来确定。 (3)求总天数。分为经历完整的月份天数与经历不完整的月份天数。 (4)输出数据。年月日及相应的天数。
主控模块 输 入 求总天数 输 出 判断闰年 求某月的天数 程序模块结构图 (1)判断闰年。 程 序 实 现: int leap(int year) {int lp; lp=(year%4==0&&year%100!=0 ||year%400==0)?1:0; return lp;}
(2)求某月的天数。 /*函数month_days调用函数leap判断是否为闰年*/ int month_days(int year,int month) { int ds,d; switch(month) {case 1: case 3: case 5: case 7: case 8: case 10: case 12:d=31;break; case 2:d=leap(year)?29:28;break; /*若为闰年,d赋值29,否则赋值28*/ default:d=30;} return d; }
(3)求天数和。 /*函数days调用month_days求各月份对应的天数*/ int days(int year,int month,int day) {int i,ds=0; for (i=1;i<month;i++) ds=ds+month_days(year,i); ds=ds+day; return ds; }
(4)主函数: void main() { int year,month,day,t_day; printf("Input year-month-day:\n“); scanf("%d-%d-%d“,&year,&month,&day); t_day=days(year,month,day); /*求天数和*/ printf(“%d-%d-%d is %dth day of the year!\n” ,year,month,day,t_day); } 注意:在完整的程序中,前三个函数应放在main( )函数之前。
二. 模块设计原则 1. 模块相对独立性 (1)模块实现相对独立的特定子功能。模块的功能单一、任务明确,函数可以相互调用但定义相互独立,一个函数不从属于另一个函数,。 (2)模块之间的关系简单。模块间只通过数据传递发生联系,且传递的数据个数越少越好。例如不能使用goto语句跳到另一个函数,以保证函数的独立性。 (3)模块内数据的局部化。模块内使用的数据具有独立性,一个模块不允许使用其它模块的数据,且一个模块的数据也不能影响其他模块中的数据。例如,C语言的局部变量就可以满足模块内数据局部化的要求。
2. 模块大小适中 3. 模块分解层次清楚 模块化程序设计要求对问题进行逐层分解、逐步细化,形成模块的层次结构。。 分解问题时要注意对问题进行抽象,将问题中的相似方面集中和概括起来,暂时忽略它们之间的差异,采取自上而下、逐步求精的方法实现.
main() {printstar(); printmessage(); printstar(): } printstar() { printf("***************\n");} print_message() { printf (" How do you do?\n"); } *************** How do you do? *************** main a c b d e f g h h i e g 一个较大的程序是由若干个程序模块组成,用子程序实现模块 的功能,在C语言中,子程序的作用是由函数实现的。 例
函数说明: (1)一个C程序由一个或多个源程序文件组成。 (2)一个源程序文件又由一个或多个函数组成.以源文件作为编译单位。 (3)C程序从main函数开始执行. 在main函数中结束函数的运行。主函数不能被其它函数调用,但其他函数可互相调用. (4)所有的函数都是平行的,不允许嵌套定义函数。 (5)用户使用的角度 ( 标准函数、自定义) (6)函数的形式 (无参函数、有参函数)
从函数使用的角度来看,C语言的函数可以分为两类:标准库函数和用户自定义函数。从函数使用的角度来看,C语言的函数可以分为两类:标准库函数和用户自定义函数。 一. 标准库函数与头文件 • Turbo C系统提供了400多个标准库函数(参见附录Ⅲ),按功能可以分为: • (1)类型转换函数 (2)字符判别与转换函数 • (3)字符串处理函数 (4)标准I/O函数 • (5)文件管理函数 (6)数学运算函数等。 它们的执行效率高,用户可在程序中直接进行调用。
2. 头文件 C语言库函数所用到的常量、外部变量、函数类型和参数说明,都在相应的头文件(扩展名为.h)中声明,这些文件通常存放在系统目录tc\include。如: (1) stdio.h文件:标准输入输出函数所用的常量、结构、宏定义、函数的类型、参数的个数与类型的描述。 (2) math.h文件:与数学函数有关的常量、结构及相应的函数类型和参数描述。 (3) string.h文件:与字符串操作函数有关的常量、结构以及相应的函数类型和参数描述。 (4) stdlib.h文件:与存储分配、转换、随机数产生等有关的常量、结构以及相应函数的类型和参数描述。 (5) ctype.h文件:字符函数有关的常量、宏定义以及相应函数的类型和参数描述。
二. 用户自定义函数 1. 函数分类 (从函数的形式看) ◆函数定义时无参数说明 无参函数 ◆调用无参函数一般用来执行指定的一组操作 ◆主调函数不传送数据给被调函数 ●函数定义时定义了一个或一个以上的参数 有参函数 ●调用时将要处理的数据传送给被调函数 ◆定义时既无参数也无执行语句 空函数 ◆被调用时,不执行任何操作就立即返回
§8.2 函数定义的一般形式 1. 无参函数的定义形式: 类型标识符 函数名 ( ) {说明部分 语句} 2. 有参函数的定义形式: 类型标识符 函数名 ( 形式参数表列) { 说明部分 语句}
int max (int x , int y) {int z ; z=x>y? x : y ; return (z); } z并不是形式参数,而是一般变量,所以在说明部分定义. 如果在定义函数时不指定类型,系统隐含指定函数为 int 型。
3.空函数的形式: 类型说明符 函数名( ) { } 此语句的功能是使程序结构清楚,可读性好,以后扩充新功能方便。如: dummy() { }
传统形参说明方式: 函数值类型名 函数名(形式参数列表) 形式参数说明 {数据说明部分 语句 } 4.传统与现代的形参的说明方式 如: int max(a,b) int a,b; 建议使用 现代形参说明方式: 函数值类型名 函数名(形式参数类型及参数说明) {数据说明部分 语句 } ANSI风格 如: int max(int a,int b)
5. 函数由函数说明与函数体两部分构成。 (1)函数说明 ◆函数说明包括函数值类型、函数名、参数类型及参数说明. ■函数说明又称为函数首部。 ◆函数值类型指定所定义函数返回值的类型,可以是简单类型、void类型或构造类型等。 ■当函数值类型为void时,表示函数无返回值,相当于其它语言的过程。当函数值类型为int时,可省略其类型的说明,建议不使用缺省形式类型说明.
◆函数名是函数的标识符,遵循C语言标识符的命名规则,区分大小写。◆函数名是函数的标识符,遵循C语言标识符的命名规则,区分大小写。 ■形式参数简称形参,处在函数名后的一对圆括号中。要特别注意的是,无论函数是否有形式参数,函数名后的圆括号不可省,并且圆括号之后不能接“;”。 ◆形式参数属于所在函数的局部变量,其存储类型只能是auto型或register型,缺省为auto型。
(2)函数体 ◆函数说明之后的花括号“{}”部分为函数体。 ■函数体内数据说明在前,执行语句在后。 ◆函数体中说明的变量是该函数调用时有效的局部变量,执行语句是实际生成命令代码的部分。 ■函数的功能由函数体内的各个语句来实现。 ◆函数体结束在“}”括号处。
例: 定义符号函数sign。 sign(x) /*函数返回值类型未说明,默认为int*/ /*建议给出函数类型说明*/ int x; /*形式参数说明*/ { int y;/*函数体局部变量*/ y=x>0?1:(x==0?0:-1); return y; /*返回函数值*/ }
6. 函数main ■一个C语言程序至少包含一个函数,并且必须有且只能有一个名为main的函数,称之为主函数。 ◆在包含多个函数的程序中,不仅可以由主函数调用其它函数,还可以由被调函数调用其它函数,但任何函数都不能调用主函数。 ■通常主函数的类型定义为void(在TC2中可省略函数main的类型说明)。
◆在具有多个函数的C程序中,主函数出现的位置并不重要。为阅读方便,可将主函数main放在最前面。为了避免过多的函数声明语句,习惯上将主函数放在所有函数之后。不论主函数放在什么位置,一旦启动该程序,总是从主函数开始执行,并且最终在主函数结束整个程序的执行。◆在具有多个函数的C程序中,主函数出现的位置并不重要。为阅读方便,可将主函数main放在最前面。为了避免过多的函数声明语句,习惯上将主函数放在所有函数之后。不论主函数放在什么位置,一旦启动该程序,总是从主函数开始执行,并且最终在主函数结束整个程序的执行。
§8.3 函数参数和函数的值 8.3.1 形式参数和实际参数 形参:在定义函数时函数名后面括号中的变量名 实参:在调用函数时函数名后面括号中的表达式 例如: c=max( a , b ) (主调函数) int max (int x ,int y ) (被调函数) {int z ; z=x>y ? x : y ; return (z); } a , b 为实际参数 x , y 为形式参数
形式参数 (简称形参) 定义函数使用的参数 实际参数 (简称实参) 调用函数使用的参数 ◆C语言中,采用函数之间的参数传递方式或用全局变量共享数据方式,使一个函数能对不同的数据进行相同功能的处理。 ■函数的参数采用单向值传递方式(或称复制方式)。 ◆单向值传递方式是指在函数调用时,将实参之值传递给对应的形式参数,使形参具有与实参相同的值。 ■当实际参数是变量的地址值、指针常量或指针变量时,实际参数传递给形式参数的是地址值,也同样是单向值传递方式。
调用函数时的数据传递 main( ) { int a,b,c; scanf(“%d,%d”,&a,&b); c=max(a,b); printf(“max is %d”,c); } max (int x, int y) { int z; z=x>y? x:y ; return(z); } 运行结果: 7,8 max is 8
有关实参与形参的几点说明: (1)在函数定义中指定的形参,未调用时,它们不占用存储单元。只有调用该函数时,形参才被分配空间,函数调用结束后,形参所占的存储单元被释放。 (2)实参为表达式。函数调用时,先计算表达式的值,然后将值传递给形参。常量、变量、函数值都可看成是表达式的特殊形式。 (3)定义函数时,形参的排列没有次序要求,但对形参列表中每个参数要进行说明。调用函数时,实参类型、个数及排列次序应与形参一一对应。若类型不一致,必须在参数前加上强制转换符,否则会发生“类型不匹配”的错误。 (4)实参与形参的数据传递为单向传递,只可由实参向形参传递,不能由形参传回实参。实参与形参处在不同的函数中,作用的区域不同,即使实参与形参同名,也是不同的变量。
2 • 3 b • 2 • 3 a b a y x • 2 • 3 x • 10 • 15 y (5)实参同形参的数值传递是“值传递” , 由实参传给形参,不能反向,即单向传递. 主调函数 被调函数 实参与形参数据传递 函数调用时,系统对参数的处理步骤为: (1)计算各实参的值,将实参值压入形参栈中,然后执行函数体。 (2)当函数执行完返回时,形参从栈顶弹出(取走)。
例:求方程ax2+bx+c=0(a0)的实数根。 分析: (1)一元二次方程的实根的条件是: 。 (2)定义函数dict来判断数方程是否有实根,有实根则返回函数值1,否则返回函数值0;然后在主函数中求方程的实根。 #include <stdio.h> #include <math.h> void main() { float a,b,c,x1,x2,d,dt; int dict(float,float,float); /*声明函数dict及形式参数类型*/ printf("Input a,b,c:"); scanf("%f,%f,%f",&a,&b,&c);/*输入方程*/ d=dict(a,b,c);/*调用函数dict,传递实参a,b,c*/ dt=b*b-4*a*c; /* ,可以用一个函数实现*/
if(d) {x1=(-b+sqrt(dt))/(2*a); x2=(-b-sqrt(dt))/(2*a); printf("实根x1=%f,x2=%f\n",x1,x2);} else printf("无实数根!\n"); } int dict(a,b,c) /*定义函数dict及形参说明*/ float a,b,c; {float d; d=b*b-4*a*c; /*可以用一个函数实现*/ if(d>=0) return(1); else return(0); }
a b c a b c 函数main 函数dict 实参与形参数据传递 通过函数调用,将实参的值传递给形参.
8.3.2 函数的返回值 1)通过return 语句来实现的. 实现形式: return (z) == return z return 后面可以是表达式,如: max(x, y) { return (x>y :x:y);} (2)函数值的类型:在定义函数时指定函数值的类型.不加说明时,自动按整型处理. int max(x ,y) char letter(c1,c2) min(x ,y)
(3) 函数值的类型同return语句中的表达式 不一致时,以函数类型为准.对数值型数据, 可以自动进行类型转换 • 例如 (4) 函数没有return语句时,返回的是一个不确定的值. {int a,b,c; a=printstar(); b=print_message(); c=printstar(); printf ("a=%d,b=%d,c=%d\n",a,b,c); } a,b,c表示输出字符的个数。
◆当函数类型为int型并且return语句的表达式缺省或return语句缺省时,返回不确定的值(在TC2中返回值为0)◆当函数类型为int型并且return语句的表达式缺省或return语句缺省时,返回不确定的值(在TC2中返回值为0) ■当函数类型为float型并且return语句缺省时,调用出错。
(5)为了明确表示“不带回值”可用“void” 定义“无类型” 如: void printstar ( ) {….} 在使用此语句后,就不能调用 eg: a=printstar ( ) ; () 因为printstar ( ) 语句无返回值. main() { int a; a=printstar(); printf (“%d\n”,a); } void printstar() {printf(“********\n”); }
§8.4 函数的调用 8.4.1 函数调用的一般形式 函数名 (实参表列) ; 实参与形参按顺序对应,一一传递数据.在C语言中按自右向左的顺序求值. 例8 .4 main() int f(int a,int b) {int i=2,p; {int c; if(a>b) c=1; p=f(i,++i); else if (a= =b c=0;) printf("%d",p);} else c=-1; return c;} 如: int i=1; printf(“%d,%d”,i,i++); 不等同于 int i=1; printf(“%d,%d”,i,++i); • 结果: • 0 为避免不确 定的情况: j=i; k=++i; p=f(j,k);
◆实际参数也称为实在参数,简称为实参,实际参数之间以“,”分隔。◆实际参数也称为实在参数,简称为实参,实际参数之间以“,”分隔。 ■函数调用时,实参与形参应保持个数、次序及类型的一致性,以确保实参与形参之间数据的正确传递。 ◆实际参数一般为表达式,可以是常量、变量(调用时必须有确定的值或确定的地址)。 ■形式参数必须为变量。 注意 当实际参数的个数、次序、类型与对应形式参数的个数、次序、类型不一致时,系统并不提示错误,后果却难以预测。
8.4.2 函数调用的方式 在函数调用后加“;”,构成一个语句。 函数语句调用 调用函数的目的可能是执行一个动作或完成特定的功能。 大多数函数的调用形式。 被调用函数执行的结果为调用函数提供一个值,除非一个函数的类型说明为void型。 函数表达式调用 调用函数通过表达式接收值。 函数参数调用 被调函数作为某个函数的一个参数。
例: 函数语句调用与函数表达式调用。 #include <stdio.h> void main() {printf("\nk1=%d",printf("\n%k2"));} 程序运行结果为: k2 k1=3
例:函数参数调用形式调用函数max2: int max2(int a,int b) /*求两个数中较大者*/ { int y; y=(a>b)?a:b; return y;} void main( ) /*主函数中采用函数参数调用形式调用函数max2*/ { int x,y,z,m; scanf("%d,%d,%d",&x,&y,&z); printf("max=%d\n",max2(max2(x,y),z)); / *内层函数max2的值作为外层函数max2的实参*/ / *整个函数max2的值又作函数printf的的实参*/ }
8.4.3 对被调函数的声明和函数原型 (1)函数必须是已经存在的函数. (2)使用库函数时,用#include命令,包含相应的头文件. #include <stdio.h> #include <math.h> (3)使用用户自定义函数,应在调用之前加以说明. 类型名 函数名(类型1 形参1,类型2 形参2,…,类型n 形参n); float max( int a,int b) 类型名 函数名(类型1,类型2,…,类型n); float max( int ,int )
例8.5: main( ) {float add(float x, float y); /* 被调函数类型说明 */ float a ,b ,c ; scanf(“%f,%f”,&a,&b); c=add(a ,b); printf(“sum=%f”,c) ;} float add (float x,float y) { float z; z=x+y ; return (z); } 3.5,6.5 • 对函数的说明也可以: • float add(float , float); sum= 10.000000
◆函数声明是以语句形式出现的,因此其后有语句结束标记“;”。◆函数声明是以语句形式出现的,因此其后有语句结束标记“;”。 ■若函数定义放在主调函数之前,遵循先定义后调用原则,函数声明可以省略。 ◆在文件的开头,函数的外部已说明函数类型时 ,函数声明可以省略。
例如: float add (float x, float y) {float z; z=x+y ; return (z); } main( ) { float a ,b ,c ; scanf(“%f,%f”,&a,&b); c=add(a ,b); printf(“sum=%f”,c) ; } 3.5,6.5 sum= 10.000000
char letter(char,char ); float f(float,float); int i( float,float); main( ) { f(3.4,5.6); i(5.1,34.5); letter(‘a’,’b’); } float f(c1,c2) {….} int i(j,k) {….} char letter(c1,c2) {……}
§8.5 函数的嵌套调用 所谓函数的嵌套调用是指一个函数调用另一函数的过程中又出现对其它的函数调用。 C语句不能嵌套定义函数,但可以嵌套调用函数。 这种嵌套调用的层次原则上不限制。 函数1 函数2 函数3 {…… ① ② {…….③ ④ {…... ⑤ 调用函数2 调用函数3 } …… ⑨ ⑧ …….⑦ ⑥ } } 调用的例子:
§8.6函数的递归调用 递归调用:在调用一个函数的过程中又出现直接或间接的调用该函数本身的过程. Eg: 直接调用: int f(int x) {int y,z; … z=f(y); return (2+z); } 间接调用: int f1(x) int f2(t) int x; int t; {int y,z; …. {int a,c; …. z=f2(y); …. } c=f1(a); …. }