620 likes | 737 Views
第五章 函数. 本章要点. 函数定义和函数返回值 函数调用与参数传递、函数原型 函数的递归调用 变量的作用域和存储类别. 本章学习目标. 了解模块化设计方法 掌握函数定义和函数原型说明的方法及二者的区别 掌握函数的参数传递方式与函数调用方法 了解函数的递归调用 理解掌握变量的作用域和存储类的概念. 5.1 什么是函数. 在 C 语言中,函数就是组合在一起的一系列语句,并且可以通过一个名字调用它。 函数这个术语来自于数学 f(x)=x 2 +2x+1 ,
E N D
本章要点 • 函数定义和函数返回值 • 函数调用与参数传递、函数原型 • 函数的递归调用 • 变量的作用域和存储类别
本章学习目标 • 了解模块化设计方法 • 掌握函数定义和函数原型说明的方法及二者的区别 • 掌握函数的参数传递方式与函数调用方法 • 了解函数的递归调用 • 理解掌握变量的作用域和存储类的概念
5.1 什么是函数 • 在C语言中,函数就是组合在一起的一系列语句,并且可以通过一个名字调用它。 • 函数这个术语来自于数学 f(x)=x2+2x+1, • 其中f是函数名,x是自变量,f(x)是对应于自变量x的函数值,式子x2+2x+1则给出了由x值得到f(x)值的计算规则。 • 数学函数中的自变量在C语言中通常称为函数参数。 • C语言中的函数可以没有参数,也可以不返回函数值。
5.2 函数的定义和调用 • 5.2.1计算两个实数的平均值 【例5-1】计算两个实数的平均值 /* 程序5-1 */ #include <stdio.h> float average(float a, float b) { float c; c=(a+b)/2; return c; } void main( ) { float x,y,z; scanf(“%f%f”,&x,&y); z=average(x,y); printf(“The average is %f”,z); }
【例5-2】显示提示信息 /* 程序5-2 */ #include <stdio.h> void showinfo( ) { printf(“This is a minus.\n”); } void main( ) { int a,b; scanf(“%d%d”,&a,&b); if(a<0) showinfo( ); if(b<0) showinfo( ); } • 5.2.2 显示提示信息
5.2.3函数的定义 • 函数定义的一般形式: 类型 函数名 (形式参数列表) { 声明 语句 } • 函数的定义可以分为两部分,函数首部和函数体。
在函数首部通常依次包含函数类型、函数名及形式参数的定义。 • 类型指定是函数返回值的类型。函数可以有,也可以没有返回值。如果忽略函数返回值类型,则返回值类型为int类型。如果函数没有返回值,应该将类型定义为void。 • 形式参数列表依次说明了每个形参的类型和名称。函数可以有多个形参,也可以没有形参。多个形参之间用逗号分隔。即使没有形参,这一对括号也不能省略。
函数的函数体中可以包含内部变量的定义和执行语句,它给出了函数功能实现的细节。函数的函数体中可以包含内部变量的定义和执行语句,它给出了函数功能实现的细节。 • 函数体也可以为空 void showinfo( ) { }
5.2.4 return语句 • return语句的作用是从被调用函数返回到主调函数中产生调用的地方。return语句会结束被调用函数的执行,使程序返回到主调函数中去执行。此外,使用return语句,还可以带回一个函数返回值。 • 对于非void类型的函数必须使用return语句给出函数的返回值。return语句的语法格式为: return 表达式; • 这里的表达式有时是常量或变量,有时是复杂的表达式。return语句返回表达式的值。
float average(float a, float b) { float c; c=(a+b)/2; return c; } float average(float a, float b) { return (a+b)/2; }
如果return语句中表达式的类型和在函数首部指定的返回值类型不一致,那么系统会把表达式的类型隐式转换成返回值类型。例如:如果return语句中表达式的类型和在函数首部指定的返回值类型不一致,那么系统会把表达式的类型隐式转换成返回值类型。例如: int ave(float a, float b) { float c; c=(a+b)/2; return c; } 例如函数调用ave(1.0,2.0)的返回结果为1。
5.2.5函数的调用 • 函数调用的格式为: 函数名(实际参数列表) • 多个实际参数用逗号分隔。 • 实际参数表示要传递给被调函数的数据。实参可以是变量、常量或任何正确的表达式。 • 实参的个数应和形参的个数相同,并且实参要和对应的形参类型一致,否则将自动进行类型转换,当然也可以在调用时,使用强制类型转换来使实参的类型与形参一致。 • 调用没有形参的函数也不需要提供实参,小括号内可以为空,但小括号是必须的。
对于void类型的函数,其函数调用是一个语句 showinfo( ); • 对于非void类型的函数,其调用可以包含在表达式中,其函数返回值可以赋值给变量,或用于接下来的运算,或用于输出,或其他用途。 ave=average(a,b); sum=average(a,b)+average(c,d); printf(“%f”,average(a,b)); average(a,b);
5.3 函数的声明 /* 程序5-3 */ #include <stdio.h> void main( ) { float x,y,z; scanf(“%f%f”,&x,&y); z=average(x,y); printf(“The average is %f”,z); } float average(float a,float b) { float c; c=(a+b)/2; return c; } • 前面的程序总是把被调函数的定义放在main函数之前,也就是放在函数调用出现之前。实际上C程序中的函数是没有固定顺序的。可以将被调函数的定义放在函数调用之后。 • 这时,C语言要求在函数调用前对被调函数进行声明。这样可以帮助编译器进行更全面、更严格的语法检查,避免一些程序错误。
/* 程序5-4 */ #include <stdio.h> float average(float a,float b); /*函数声明*/ void main( ) { float x,y,z; scanf(“%f%f”,&x,&y); z=average(x,y); /*函数调用*/ printf(“The average is %f”,z); } float average(float a,float b) /*函数定义*/ { float c; c=(a+b)/2; return c; } • 程序5-3增加函数声明后得到的程序如下。
可以看出,函数声明类似于函数定义的首部,区别是后面要带有一个分号。可以看出,函数声明类似于函数定义的首部,区别是后面要带有一个分号。 • 函数声明的格式为: 类型 函数名(形式参数列表); float average(float a,float b); 或 float average(float ,float );
void main( ) { float average(float a,float b); /*函数声明*/ float x,y,z; scanf(“%f%f”,&x,&y); z=average(x,y); /*函数调用*/ printf(“The average is %f”,z); } • 函数声明即可放在函数体外,也可以放在函数体内。在程序5-4中,对average函数的声明也可以放在main函数体内。 • 此时,函数声明仅对main函数有效,其它函数如需调用average函数,也要增加类似声明。 • 注意:函数声明也称为函数原型,函数声明必须和函数定义一致。
x 12 12 a 34 34 y b 5.4 函数的参数传递 • 形参实际上是指定类型的变量,而实参可以是变量、常量或由运算符连接的表达式。 • 实参向形参的数据传递是值传递,在调用函数时,将计算出每个实参的值,然后将值赋值给对应的形参变量,作为形参变量的初始值。 • 如在程序5-4中,main函数调用average函数
/* 程序5-5 */ #include <stdio.h> void swap(int x,int y) { int temp; temp = x; x = y; y = temp; printf("x=%d,y=%d\n", x, y); } void main() { int a = 3, b = 4; swap(a, b); printf("a=%d,b=%d\n", a, b); } 程序运行后输出结果为: x=4,y=3 a=3,b=4 • 函数执行过程中,如果形参的值发生了变化,这种变化不会影响对应实参的值。所以值传递是一种单向传递。 • 【例5-3】参数传递
a 3 4 b a 3 3 x 4 4 b y • 程序执行过程如下: ①main函数定义变量a、b并初始化。 ②main函数调用函数swap,实参a、b将值传递给形参x、y。
a 3 4 b 4 x 3 y ③swap函数交换x、y的值。 ④返回main函数,输出a、b的值,a、b的值没有改变。
5.5 递归 • 5.5.1函数的递归调用 • 函数直接或间接调用自己就称为函数的递归调用。 • 5.5.2递归的思想 • 汉诺塔问题 • 递归的思想:将复杂问题分解为规模较小的子问题并且子问题和原问题本质上是相同的问题。子问题求解后,原问题也将求解。
5.5.3递归的使用 • 问题:使用递归方法计算n! • n!的定义是: • 这种定义方式体现了递归的思想,求n!可以转化为先求(n-1)!。
下面以求5!为例说明求解过程。 • 递推过程 • 5!=4! ×5 • 4!=3! ×4 • 3!=2! ×3 • 2!=1! ×2 • 1!=1 • 回归过程 • 2!=1×2=2 • 3!=2×3=6 • 4!=6×4=24 • 5!=24×5=120
/* 程序5-8 */ #include <stdio.h> int fac(int n) { if(n==1) return 1; else return fac(n-1)*n; } void main( ) { printf(“The factorial of 5 is %d”,fac(5)); } • 可以看出一个递归过程可以分为“递推”和“回归”两个阶段。“递推”阶段进行问题分解,将复杂问题分解为性质相同、规模较小的简单问题,简单问题可做同样的分解直到分解后的问题可直接求解。“回归”阶段是根据已得到的简单问题的解一步一步返回,直到得到原问题的解。 • 【例5-6】使用递归方法求解n! • 递归过程应该有结束递归调用的边界条件,如程序5-8中的n==1,当不满足边界条件时,产生新的递归调用;当满足边界条件时,不产生新的递归调用,递归调用返回。
5.6 局部变量与全局变量 • 5.6.1程序块 • 复合语句 { 变量定义 多条语句 } • 我们将这样的复合语句称为一个程序块。 • 函数的函数体也是一个程序块。在函数体内也可以含有内部程序块。 if (x>y) { int temp; temp=x; x=y; y=temp; }
5.6.2局部变量 • 在函数体内定义的变量称为函数的局部变量。 float average(float a,float b) { float c; c=(a+b)/2; return c; }
默认情况下,局部变量具有以下两个特性: • (1)动态存储期限。 • 动态分配存储空间 • 只在函数执行期间是存在的, • 在两次函数调用之间,局部变量不能保留原来的值。 • (2)程序块作用域。 • 作用域仅限于函数内体内, • 在其它函数中是无法通过变量名直接访问该变量的。 • 可以在其他函数中定义同名的变量。
main函数中定义的变量也属于局部变量。 • 函数的形式参数也是局部变量。 • 准确的说在程序块(包括函数体)中定义的变量都属于局部变量。程序块中的变量的作用域限定在定义该变量的程序块内。
/*程序5-10*/ #include <stdio.h> int maxvalue(int a, int b) { if(a<b) { int temp; temp=a; a=b; b=temp; } return a; } void main( ) { int a,b,max; printf(“Input a and b:\n”); scanf(“%d,%d”,&a,&b); max=maxvalue(a,b); printf(“The max value is:%d\n”,max); } • 【例5-7】求两个整数的最大值
5.6.3外部变量 • 我们把在函数之外定义的变量称为外部变量或全局变量。 float ave; float average(float a,float b) { ave=(a+b)/2; }
外部变量具有和局部变量不同的两个特性: • (1)静态存储期限。在程序执行过程中,外部变量始终具有固定的存储空间,所以可以永久保留变量的值。 • (2)文件作用域。外部变量的作用域是从变量的定义开始一直到程序文件的结束。所以在外部变量定义之后的所有函数都可以通过变量名访问它。
/*程序5-11*/ #include <stdio.h> int gcd, lcm; void fun ( int m, int n) { int a,b,r; a=m; b=n; do { r=m%n; m=n; n=r; }while(r!=0); gcd=m; lcm=a*b/gcd; } void main( ) { int m,n; scanf("%d,%d",&m,&n); func(m,n); printf("GCD:%d\n",gcd); printf("LCM:%d\n",lcm); } • 外部变量可以被多个函数所共享。 • 【例5-8】求两个整数的最大公约数和最小公倍数。 • 尽管外部变量可用于函数之间交换数据,但应该尽量减少外部变量的使用,函数之间应该通过参数和返回值实现数据交换。
5.6.4作用域规则 /*程序5-12*/ #include <stdio.h> int k=3; /*第1次定义*/ void func( ) { int k =7; /*第2次定义*/ printf("k=%d\n", k ); { int k=11; /*第3次定义*/ k+=1; printf("k=%d\n",k); } printf("k=%d\n", k ); } void main( ) { func( ); printf("k=%d\n", k ); } 程序运行后输出结果为: k=7 k=12 k=7 k=3 • 作用域规则是当外部变量与局部变量同名时,在局部变量的作用域内,外部变量将不起作用,也就是局部变量会屏蔽掉外部变量。
5.7 变量的存储类别 • 变量定义的语法格式 [存储类别标识符] 类型说明符 变量名列表; • 存储类别标识符一共有4种:auto、register、static、extern。 • 在变量定义时,最多只能出现一个存储类别标识符。 auto int i; static float f,g; extern int a,b,c;
5.7.1变量的性质 • 定义变量时,主要说明了变量的三种性质。 • (1)存储期限。动态存储期限和静态存储期限两种。 • (2)作用域。程序块作用域和文件作用域两种。 • (3)连接。变量的连接性质说明了变量可以被共享的范围。变量的连接分为外部连接、内部连接和无连接三种。具有外部连接的变量可以被程序的多个源文件共享使用,具有内部连接的变量只能被一个源文件使用(可以被一个源文件中的多个函数共享使用),无连接的变量只能在一个函数内使用。
定义变量时,如果省略了存储类别标识符,将根据变量定义的位置来确定这三种性质。定义变量时,如果省略了存储类别标识符,将根据变量定义的位置来确定这三种性质。 • (1)局部变量具有动态存储期限、程序块作用域、无连接 • (2)外部变量具有静态存储期限、文件作用域、外部连接 • 如果需要,可以指定变量的存储类别,来改变这三种性质。
5.7.2 auto • auto存储类别只能用于局部变量的定义。定义局部变量时,如果没有特别指明它的存储类别,则该变量的存储类别就是auto,所用auto关键字是可以省略的。 fun( ) { auto int a; } fun( ) { int a; }
5.7.3 register • register存储类别只能用于局部变量的定义。 • register存储类别的变量和auto存储类别的变量具有一样的动态存储期限、程序块作用域、无连接。 • 指定变量具有register存储类别的目的是要求编译器将变量存放在寄存器(寄存器是CPU的内部存储单元)中,而不是存放在内存中。 • register存储类别目前已经很少使用了 。
5.7.4 static • static关键字既可以用于局部变量的定义,也可以用于外部变量的定义。 • (1)static用于局部变量 • 将局部变量定义为static存储类别,会使得变量的存储期限由动态存储期限变成静态存储期限,同时变量仍具有程序块作用域和无连接的性质。
static用于局部变量 /*程序5-13*/ #include <stdio.h> int fun( ) { int x=0; static int t=0; x++; t++; return (x+t); } void main() { int i; for(i=1;i<=3;i++) printf(“%d\n”, fun( )); } 程序运行后输出结果为: 2 3 4 • static类别的局部变量会在程序运行期间一直占用固定的存储空间,变量值可以持久保存。 • static类别的局部变量只在程序开始执行前进行一次初始化(如果没有提供初始值,编译器会将其自动初始化为0)。 • 【例5-10】使用static局部变量
static int a,b; void f1( ) { a++; b*=2; } void f2( ) { b=a; b++; } • (2)static用于外部变量 • 将外部变量定义为static存储类别,会使得变量具有内部连接的性质,同时变量仍具有静态存储期限和文件作用域的性质。 • static类别的外部变量只能被其所在源文件内的函数来使用,而不能被其它程序文件中的函数使用。 函数f1和f2可以使用变量a、b, 但其它文件中的函数无法使用变量a、b。
5.7.5 extern • extern存储类别用于对已定义的外部变量进行声明,以便在多个源文件中共享同一个外部变量。 • 在一个程序中,一个外部变量(没有指定为static存储类别 )只能定义一次。 int a=1; • 但可以出现多次对该变量的声明。 extern int a;
/*程序5-14*/ #include <stdio.h> extern int B; void fun1( ) { extern int A; A=10; B=10; } void fun2( ) { B=20; } int A,B; void main( ) { A=1; B=1; fun1( ); fun2( ); printf(“A=%d,B=%d\n”, A, B); } 程序运行后输出结果为: A=10,B=20 • 利用extern对外部变量进行声明,使得我们可以在外部变量作用域之外的地方使用它。 【例5-11】使用extern对外部变量进行声明
更常见的情况是,我们需要访问在另外一个源文件中定义的外部变量。这同样通过对外部变量进行声明来实现。 【例5-12】使用extern对外部变量进行声明 /*file1.c的内容*/ #include <stdio.h> int A=1; void main( ) { void fun1( ); /*函数fun1的声明*/ printf(“A=%d\n”, A); fun1( ); printf(“A=%d\n”, A); } /*file2.c的内容*/ extern int A; void fun1( ) { A=10; } 程序运行后输出结果为A=1 A=10
习 题 (1)以下关于函数的叙述中正确的是( ) A)每个函数都可以被其它函数调用(包括main函数) B)每个函数都可以被单独编译 C)每个函数都可以单独运行 D)在一个函数内部可以定义另一个函数
(2)以下叙述中错误的是( ) A) C程序必须由一个或一个以上的函数组成 B) 函数调用可以作为一个独立的语句存在 C) 若函数有返回值,必须通过return语句返回 D) 函数形参的值也可以传回给对应的实参
(3)在C语言中,函数返回值的类型最终取决于( ) A)函数定义时在函数首部所说明的函数类型 B)return语句中表达式值的类型 C)调用函数时主调函数所传递的实参类型 D)函数定义时形参的类型。
(4)若函数调用时的实参为变量时,以下关于函数形参和实参的叙述中正确的是( ) A)函数的实参和其对应的形参共占同一存储单元 B)形参只是形式上的存在,不占用具体存储单元 C)同名的实参和形参占同一存储单元 D)函数的形参和实参分别占用不同的存储单元