430 likes | 598 Views
第 5 章 函数. 函数的定义 函数调用的方法 函数之间的数据传递 函数的递归调用 变量的作用范围和存储类别. 5.1 函数的分类. 从用户的观点出发, C 语言的函数分为库函数和用户自定义函数两种。. 库函数: C 语言提供了丰富的库函数。正确调用这些已有的标准库函数。 用户自定义函数:用户为解决专门的问题而编写的函数。(本章的重点). 从函数间数据传递的关系出发, C 语言的函数可分为有参函数和无参函数两种。. 有参函数:多数函数都有一个或多个形式参数,此类函数称为有参函数。形式参数用于函数之间的数据通信。
E N D
第5章 函数 • 函数的定义 • 函数调用的方法 • 函数之间的数据传递 • 函数的递归调用 • 变量的作用范围和存储类别
5.1 函数的分类 • 从用户的观点出发,C语言的函数分为库函数和用户自定义函数两种。 • 库函数:C语言提供了丰富的库函数。正确调用这些已有的标准库函数。 • 用户自定义函数:用户为解决专门的问题而编写的函数。(本章的重点) • 从函数间数据传递的关系出发,C语言的函数可分为有参函数和无参函数两种。 • 有参函数:多数函数都有一个或多个形式参数,此类函数称为有参函数。形式参数用于函数之间的数据通信。 • 无参函数:没有定义形式参数的函数称为无参函数。这些函数在调用时无需提供任何输入信息。 • 函数还可以分为有返回值函数和无返回值函数,以及内部函数和外部函数等。
5.2 函数基础 5.2.1 函数的定义 函数类型 函数名(形式参数表列) { 说明部分 语句部分 } 函数头 函数体 • 函数类型是指函数返回值的类型。若函数返回值的类型标识符省略,默认为int类型。若函数无返回值,则要将函数定义为void类型。 • 函数名是由用户命名的标识符,在同一程序中函数名必须唯一。
5.2.1 函数的定义 • 在函数头部,用圆括号括起来的是形式参数表列,形式参数简称形参。形参是函数间数据传递的载体。每个形参前有相应的类型标识符,各形参的定义之间用逗号分开: 函数类型 函数名(类型名 形参1, 类型名 形参2,……) • 函数也可以没有形参,则形式参数表列项为空,但函数名后面的一对圆括号不能省略: • 函数类型 函数名( ) • 函数体中包括了除形参外的在函数中用到的变量的定义,以及用来完成函数功能的语句。 • 函数的定义不能嵌套。 空函数: void dummy() { }
5.2.1 函数的定义 【例5-1】 编写求两个单精度实数中大数的函数。 float max(float x, float y) { float z; if(x>y) z=x; else z=y; return z; } • float max(float x, float y)为函数的头部,max是函数名。x和y为两个形参, • 函数体中,定义了一个变量z,代表x和y中的大数。 • 函数体中的最后一条语句return z;,表示返回到调用该函数的地方,并带回函数值z。
5.2 函数基础 5.2.2 函数的调用 • 在函数中可以按照一定的格式使用另一个函数,在一个函数中使用其他函数称为函数调用,前者是主调函数,后者为被调函数。 • 主调函数通过函数调用向被调函数进行数据传递,并把控制权转交给被调函数;被调函数在完成自己的任务后,会将结果返回给主调函数并交回控制权。 函数名(实际参数表列) 1.函数调用的一般形式 • 函数名是被调用函数的名字。 • 实际参数表列是主调函数传递给被调函数的输入数据。这些具有确定值的参数称为实际参数,简称实参。 • 实参的值由主调函数传递给被调函数的形参。实参表列包括多个实参时,各参数间用逗号隔开。 • 实参可以是常量、变量或表达式。 • 在调用时实参必须有确定的值。
5.2.2 函数的调用 • (1)函数表达式形式 • 函数调用可以出现在某个表达式中,是表达式运算的一部分。 c=max (5.0, 8.0); • (2)函数语句形式 • 函数调用的后面加上一个;,作为单独的语句被调用。 • fabs(-2); /*实参是常量*/ • putchar(c); /*变量c的值已知*/ • sqrt((x*x+2)*(y+z)); /*变量x、y、z的值已知*/ • (3)函数参数 • 函数调用作为另一函数调用时的实参。 m=max(a,max(b,c));
5.2.2 函数的调用 【例5-2】 编写求两个单精度实数中大数的函数。并在主函数中调用它。 #include <stdio.h> float max(float x, float y) { float z; if(x>y) z=x; else z=y; return z; } int main(void) { float a, b, c; scanf("%f%f ", &a,&b); c=max(a,b); printf("max is %f\n",c); return 0; } max(a,b)是函数调用,变量a和b是实参。
5.2.2 函数的调用 2.函数调用时的语法要求 • 函数调用时,函数名必须与所调用的函数名称完全相同。 • 实际参数的个数要与形式参数的个数一致,实参的类型要与形参表列中对应的形参的类型相同或赋值兼容。 • C语言程序可由一个主函数和若干个其他函数构成。C语言程序的执行从主函数开始,并且在主函数中结束整个程序的运行。主函数可以调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次。 • 所有函数都是平行的,即在定义函数时是分别进行的,是互相独立的。一个函数不从属于另一个函数,即函数不能嵌套定义。但函数可以嵌套调用,函数也可以直接或间接地调用自己,这称为递归调用。
5.2 函数基础 5.2.3 函数原型 • 对于用户定义的函数,遵循“先定义,后调用”的原则。 • 凡是没有在调用前定义或声明的函数,C编译程序都默认函数的返回值为int类型。所以,如果函数的定义在调用之后,应该在调用之前对函数进行声明(也称为函数原型声明)。 函数类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ……); 函数类型 函数名(参数类型1, 参数类型2, ……); float max(float x, float y); float y, x[10], max(float x, float y);
5.2.3 函数原型 【例5-3】 编写函数ftoc,完成将华氏温度转换为摄氏温度。 #include <stdio.h> float ftoc(float temperature); /*函数原型声明*/ int main(void) { float f,c; printf("input : F= "); scanf("%f", &f); /*输入华氏温度值*/ c=ftoc(f); /*调用函数ftoc*/ printf("the temperature is %f C. \n ",c); /*输出转换后的摄氏温度*/ return 0; } float ftoc(float temperature)/* 定义ftoc函数, temperature为实型形参 */ { float c; c=(5./9.)*( temperature-32); /* 换算成摄氏温度,赋给c */ return c; } /* 把结果返回主调函数*/ • 如果去掉float ftoc(float temperature);程序能运行出正确结果吗?为什么?
5.2 函数基础 5.2.4 函数的返回值 • 函数终止执行返回主调函数有以下两条途径。 • 在执行函数的最后一条语句之后。概念上到达结束函数体的右花括号}处时,函数的执行终止,返回到主调函数。 • 使用return语句。 • 被调函数执行完成后,常常需要将处理的结果返回给主调函数,这种数据传递称为函数的返回值(函数值)。函数的返回值是函数数据输出的通道之一。除了void函数外,所有的函数都应该返回一个值。函数的返回值通常采用在函数体中用return语句显式地给出。一般形式为: return [(]表达式[)];
5.2.4 函数的返回值 • return语句的功能: • 将return后面表达式的值返回给主调函数。 • 结束被调函数的执行,把程序运行的控制权交回给主调函数。 • return语句中表达式的值就是所求的函数值。此表达式值的类型必须与函数定义时的数据类型一致。如果类型不一致,则以定义时函数返回值的类型为准,由系统自动进行转换,将表达式值的类型转换为函数定义的数据类型。
5.2 函数基础 5.2.5 函数之间的数据传递 • 当函数调用时,主调函数把数据传递给被调函数,供后者使用。函数的形参通常是函数的数据输入通道。 • 在函数定义时,形参表列中的参数没有具体的给定值,系统也不会立即为其分配存储单元。 • 如果函数的形参是简单变量,在函数调用时,实参(可以是常量或具有确定值的表达式)的数据被传递给形参,这种传递方式是单向的。 • 传递的实现过程是:首先为形参在内存中开辟存储空间,然后主调函数把实参的值赋给对应的形参。这种方式称为实参和形参之间的按值传递。
5.2.5 函数之间的数据传递 • 按值传递方式具有如下特点。 • 按值传递是单向传递,只能把实参的值传递给形参变量。 • 如果实参是变量,则形参变量和实参变量具有各自的存储空间,形参接受数据后,在被调函数内部,其值可以改变,但形参变量取值的改变不能反过来影响主调函数中实参值。 • 在函数调用结束后,形参所占存储单元将会被释放。 • 实参的类型与形参的类型要相同或赋值兼容。如果不能赋值兼容,则会产生错误。
5.2.5 函数之间的数据传递 【例5-5】 程序示例:函数参数之间的单向传递。 #include <stdio.h> void try(int x, int y, int z) { printf( "2) x=%d, y=%d, z=%d\n", x, y, z); z=x+y; x=x*x; y=y*y; printf( "3) x=%d, y=%d, z=%d\n", x, y, z); } int main(void) { int a=2, b =3, c=4 ; printf( "1) a=%d, b=%d, c=%d\n", a, b, c); try( a, b, c); printf( "4) a=%d, b=%d, c=%d\n", a, b, c); return 0; }
5.2 函数基础 5.2.6 函数应用举例 【例5-7】 计算并输出从键盘上输入的多个整数中所有偶数的个数,直到输入-1时停止。判断一个整数是否为偶数要求编写函数even( int m)来完成。请编写main函数和even函数。 算法分析:根据题意,编写函数even( int m)完成判断一个数m是否为偶数。如果m是偶数,则函数返回1,否则返回0。编写main函数,输入整数并调用even函数,最后输出其中偶数的个数。
5.2.6 函数应用举例 #include <stdio.h> int even( int m) /*判断一个数是否为偶数的函数*/ { if (m%2==0) return 1; else return 0; } int main(void) { int m,s=0; printf("请输入整数,直到-1为止: "); do { scanf("%d",&m); if (even(m)) /*调用函数even() */ s++; } while (m!=-1); printf("%d\n",s); return 0; }
5.2.6 函数应用举例 【例5-9】 编写程序验证:任何一个不小于6的偶数可以表示为两个素数之和。 • 算法分析:对于任何一个不小于6的偶数n,从i=3开始直至i=n/2,在其中寻找若有一个数i,使i和n-i均为素数,则问题有解。程序中定义一个函数isprime()用于判断一个数是否为素数。 • 算法描述: • 1.定义变量n,i; • 2.输入一个数赋给n,若n为不小于6的偶数,则继续执行步骤3; • 3.i=3; • 4.判断i是否为素数,若是,执行步骤5;若不是素数,则执行步骤7; • 5.判断n-i是否为素数,若是,执行步骤6;若不是,则执行步骤7; • 6.输出结果。 • 7.i++; • 8.重复执行步骤4,直到i>n/2。
5.2.6 函数应用举例 #include <stdio.h> int isprime(int); int main(void) { int n,i; printf("请输入一个不小于6的偶数\n"); scanf("%d",&n); if (n>=6 && n%2==0) { for(i=3;i<=n/2;i++) { if (isprime(i)) if (isprime(n-i)) printf("%d=%d+%d\n",n,i,n-i); } } else printf("请输入一个不小于6的偶数\n"); return 0; } int isprime (int m) { int j; for (j=2;j<=m/2;j++) if (m%j==0) return 0; return 1; }
5.3 函数的递归调用 • 在一个函数被调用过程中又调用另一个函数称为函数的嵌套调用。如果一个函数直接或间接地调用自己,则称为函数的递归调用。直接自己调用自己称为简单递归,间接自己调用自己称为间接递归, 5.3.1 运行栈 • 函数调用过程中现场数据的保存和调用结束后现场数据的恢复是由运行栈来完成的。
5.3 函数的递归调用 5.3.2 函数的嵌套调用 【例5-10】 由用户输入两同心圆的半径值r1、r2,以求得此两同心圆组成的圆环的面积。 • main()函数 • area_ring()函数 • area()函数 • pi()函数
5.3 函数的递归调用 5.3.3 递归调用 数学函数中有一些是采用递归形式定义的, • 从n的阶乘的定义可以得出:在求解n的阶乘中使用了(n-1)的阶乘,即:要求出n! ,必须先要计算(n-1)!,而要计算(n-1)的阶乘,又必须计算(n-2)!,依次类推,直至1!=1。只有求得1!=1,再以此为基础,返回来可以计算2!,3!……(n-1)!、n!。最终得到n!的值。
5.3 函数的递归调用 5.3.3 递归调用 数学函数中有一些是采用递归形式定义的, • 从n的阶乘的定义可以得出:在求解n的阶乘中使用了(n-1)的阶乘,即:要求出n! ,必须先要计算(n-1)!,而要计算(n-1)的阶乘,又必须计算(n-2)!,依次类推,直至1!=1。只有求得1!=1,再以此为基础,返回来可以计算2!,3!……(n-1)!、n!。最终得到n!的值。
5.3.3 递归调用 • 一个问题要采用递归方法来求解,必须满足以下3个条件。 • 可以将要解决的问题转化为一个新的问题,而这个新的问题的解决仍然与原来的解法相同,只是所处理的对象有规律地递增或递减。 • 可以应用这个转化过程使问题得到解决。 • 必须要有一个明确的结束递归的条件。否则无限制地递归调用将导致程序无法结束。
5.3.3 递归调用 【例5-11】 用递归函数编程计算n!。 int fac(unsigned int n)/* 函数定义 */ { int f; if (n==1) f=1; else f=fac(n-1)*n; return (f); } #include <stdio.h> int fac(unsigned int n);/* 函数原型 */ int main(void) { int n,sum; printf ("input a unsigned interger : "); scanf ("%d",&n); sum=fac(n); printf ("%d!= %ld\n",n,sum); return 0; }
5.3.3 递归调用 input a unsigned interger : 5↙ 5!= 120
5.3.3 递归调用 • 递归函数最典型的例子是Hanoi塔问题,它能较好地显示出函数递归调用的作用。 • 汉诺(Hanoi)塔问题是一个古老的数学问题:在一个塔座(设为塔1)上有若干片盘片,盘片大小各不相等,按大盘在下、小盘在上的顺序叠放,现要将其移放至另一个塔座(设为塔2)上去。问:仅依靠一个附加的塔座(设为塔3),在每次只允许搬动一片盘片,且在整个移动过程中始终保持每座塔座上的盘片均为大盘在下、小盘在上的叠放方式,能做到么?如何移法? • 思路:对于n片盘,只要能将除最下面最大的一片盘片外,其余的n-1片盘移至塔3上,剩下一片就可直接移至塔2上。其余的n-1片盘既能从塔1移至塔3,自然也可照理从塔3移至塔2,问题就解决了。每次使用同样的办法解决最下面最大一片盘片的移动问题,一次次搬下去,直至剩下最后一片盘,直接搬到塔2上去就可以了。这个想法是成立的,是解决此问题的办法。这就是典型的递归问题,递归的结束条件是当只剩下一片盘时,可直接移至目的座(塔2)上。
5.3.3 递归调用 【例5-12】 编写程序,求解汉诺(Hanoi)塔问题。 #include <stdio.h> void hanoi_ta(int n,char ta1,char ta2,char ta3); int main(void) { int n; printf ("input the number of diskes : "); scanf ("%d",&n);/*盘片数n */ hanoi_ta (n, '1', '2', '3'); printf ("\n"); return 0; } void hanoi_ta (int n, char ta1, char ta2, char ta3)/* 函数定义 */ { if (n==1) printf("%c→%c ", ta1, ta2); /*一片盘时,直接移动*/ else { hanoi_ta (n-1, ta1, ta3, ta2); /*n-1片盘由ta1座移至ta3座*/ printf ("%c→%c ", ta1, ta2); /*最下面最大一片盘,直接移动*/ hanoi_ta (n-1, ta3, ta2, ta1);/*n-1片盘由ta3座移至ta2座*/ } }
5.4 标识符的作用域和生存期 5.4.1 标识符的作用域 • 指程序中可以访问该标识符所代表变量的区域。 • 变量的作用域与定义变量的语句在程序中的位置有关系。 • 定义变量的基本位置有3个:函数内、函数外和函数参数。 • 从变量作用域的角度来看,变量可以分为全局变量和局部变量。 1.局部变量 • 在函数内部或复合语句内部定义的变量称为局部变量, • 函数的形参也是局部变量。 • 局部变量的作用域仅限于本函数或复合语句内。
5.4.1 标识符的作用域 【例5-13】 局部变量的使用示例。 #include <stdio.h> void f2(int x,int y) { x++; y++; } void f1(int x,int y) { int n=0; f2(x, y); printf("n=%d,x=%d,y=%d\n",n,x,y); } int main(void) { int n=2,a=3,b=4; f1(a,b); printf("n=%d,a=%d,b=%d\n",n,a,b); return 0; } f2()形参x、y的作用域 f1()形参x、y的作用域 局部变量n的作用域 局部变量n、a、b的作用域
5.4.1 标识符的作用域 2.全局变量 • 在函数外部定义的变量称为全局变量。 • 全局变量的作用域是从变量定义的地方开始直至整个源程序文件的结束为止。 【例5-14】 全局变量的使用示例。 #include <stdio.h> int M=5; /*定义全局变量M*/ int fun(int x, int y) { int M=10 ; /*定义局部变量M*/ return (x*y-M) ; } int main(void) { int a=7, b=5; printf(" %d\n", fun(a,b)/M); return 0;} 局部变量M的作用域 全部变量M的作用域
5.4 标识符的作用域和生存期 5.4.2 存储类别 • 存储类别是变量的另一个属性。它涉及变量存在的时间长短,作用域的大小以及在硬件中存放它们的地点、区域等。 • 从变量值存在的时间(即生存期)来看,变量的存储有两种方式:静态存储方式和动态存储方式。 • 静态存储方式是指在程序运行期间由系统分配固定的存储空间的方式。 • 动态存储方式是指在程序运行期间根据需要进行动态地分配存储空间的方式。 • C语言的存储类别有两种:自动类和静态类。 • 自动类是局部变量默认的存储类别。 • 全局变量只能是静态类。
5.4.2 存储类别 • 供用户使用的存储空间分为3个不同的部分: • 程序区:存放程序的可执行代码模块。 • 静态存储区:存放所有的全局变量以及标明为静态类别的局部变量。 • 动态存储区(运行栈区):存放所有未标明为静态类的局部变量、函数的形式参数、现场保护 。 • 存储类别说明符: • auto(自动) • register(寄存器) • static(静态) • extern(外部) 1.自动变量 • 自动变量用存储类别说明符auto表示。 • 如果没有指定存储类别,默认为是自动变量 。
5.4.2 存储类别 • 自动变量分配在动态存储区。采用动态存储分配方法,即在函数调用时才分配存储单元,函数结束时存储单元又被回收释放。因此,自动类别局部变量的生存期是:当程序的运行进入到这个变量所在的函数体(或复合语句)时分配存储单元,退出函数体(或复合语句)释放存储单元。 【例5-15】 自动变量的使用示例。 #include <stdio.h> void f (int n) { auto int s=0; /*auto可省略写成int s=0;*/ s=s+1; printf("No.%d:s=%d\n",n,s);} int main(void) { int num; for(num=1;num<4;num++) f(num); return 0;} No.1:s=1 No.2:s=1 No.3:s=1
5.4.2 存储类别 2.寄存器变量 • 寄存器变量用存储类别说明符register表示。 • 寄存器变量也是自动类变量,它与自动变量不同的是寄存器变量的值保存在计算机CPU寄存器中。 3.静态类的局部变量 • 当在函数体或复合语句内用存储类别说明符static说明一个变量时,该变量为静态局部变量。 • 静态局部变量存放在静态存储区。一旦为其分配了存储单元,则在整个程序执行期间,将固定地占有分配的存储单元。程序执行结束时才释放。这一点和全局变量是一样的。 • 但静态局部变量的作用域与自动变量一样,只有在定义它的函数或复合语句内可以访问它。
5.4.2 存储类别 【例5-17】 静态局部变量的使用示例。 #include <stdio.h> void f (int n) { static int s=0; /* s为静态局部变量*/ s=s+1; printf("No.%d:s=%d\n",n,s); } int main(void) { int num; for(num=1;num<4;num++) f (num); return 0; } No.1:s=1 No.2:s=2 No.3:s=3
5.4.2 存储类别 4.extern说明符 • extern说明符置于变量前,用来说明变量的定义在别的文件或位置,提示编译程序遇到此变量时在其他模块中寻找其定义。 • 当全局变量的定义在后,而使用在前时,要在使用它的函数中用extern说明符对该全局变量进行说明,以便通知编译程序。这时其作用域从extern说明处起直至该函数的结尾。 【例5-18】 extern的使用示例:在同一文件中使用extern声明全局变量。
5.4.2 存储类别 #include <stdio.h> int max(int x,int y); int main(void) { int result; extern int X; /*声明全局变量*/ extern int Y; result = max(X,Y); printf("the max value is %d\n",result); return 0; } int X = 10; /*定义全局变量*/ int Y = 20; int max(int x, int y) { return (x>y ? x : y); }
5.4.2 存储类别 【例5-19】 extern的使用示例:在不同文件中使用extern声明全局变量。 /*file1.c*/ #include <stdio.h> int X; /*定义全局变量*/ extern pow(int n); int main(void) { int a=4,b=5; X=a; printf("%d\n", pow(b)); return 0; } /*file2.c*/ extern int X; /*声明全局变量*/ int pow(int n) /*求ab的函数*/ { int i,y=1; for(i=1;i<=n;i++) y=y*X; return (y); }
5.4.2 存储类别 5.静态全局变量 • 全局变量的存储类别本身就具有静态类,全局变量的生存期是整个程序的运行期间。 • 在定义全局变量时加上存储类别说明符static,并不是用来说明是静态类,而是用来说明全局变量只有在它所定义的文件内部被访问和使用,不能为本文件外其他源程序文件中的函数所使用。
5.4 标识符的作用域和生存期 5.4.3 内部函数和外部函数 1.内部函数 • 一个函数只能被其所在文件内的其他函数调用,而不能被其他文件中的函数调用,则称为内部函数。 • 标明一个函数为内部函数的方法是在定义一个函数时,在函数类型前加上static说明符。内部函数也称为静态函数。 • 内部函数不能被其他文件中的函数使用。因此,不同文件中允许使用相同名字的内部函数,这种使用不会互相干扰和影响。
5.4.3 内部函数和外部函数 2.外部函数 • 定义一个函数时,若在函数类型前加上extern说明符,则称函数为外部函数。由于函数的本质是全局的,不加extern,在C语言中也是隐含其为外部函数的。 • 外部函数可以被其他源程序文件中的函数所调用。外部函数在所有使用它的源文件中只能定义一次。而在要调用它的其他文件中,用extern加函数原型声明来加以说明。