C语言大学实用教程 第5章 函数与程序结构 西南财经大学经济信息工程学院 刘家芬 jfliu@swufe.edu.cn
程序设计的艺术 结构化程序设计 自顶向下 逐步细化 模块化设计 结构化编码 函数(function)是模块化设计的体现
用函数解决问题的要点 分而治之 复用 信息隐藏 函数把较大的任务分解成若干个较小的任务 程序员可以最大程度利用其他人的代码,而不需要做起从头 设计得当的函数可以把具体操作细节隐藏掉,从而使整个程序结构清楚
例 5.1函数示例 #include <stdio.h> void main( ) { void printstar( ); /*函数声明*/ void print_message( ); /*函数声明*/ printstar( ); print_message( ); printstar( ); } void printstar( ) printf("* * * * * * * * * * * * * * * * \n"); void print_message( ); printf(" How do you do! \n");
C中的函数(Function) 说明: 一个C程序由一个或多个源程序文件组成。 一个源程序文件由一个或多个函数组成。 C程序的执行从main函数开始,调用其他函数后流程回到main函数,在main函数中结束整个程序运行。
函数的分类 函数的使用 主函数main 标准函数,即库函数 用户自定义函数 函数的形式 有参函数 无参函数
无参函数的定义 无参函数的定义形式 类型标识符 函数名( ) { 语句 } 例:void print_message( ) { 类型标识符 函数名( ) { 语句 } 例:void print_message( ) { printf(" How do you do! \n); }
有参函数的定义 类型标识符 函数名(形式参数列表 ) { 语句 } 例:int max(int x,int y) int z; 类型标识符 函数名(形式参数列表 ) { 语句 } 例:int max(int x,int y) int z; z=x>y?x:y; return z;
关于函数 函数的类型实际上是返回值的类型,即return语句中返回的数据的类型。如果不需要返回值,应该用void标识。 函数中定义的变量只能在函数内部使用,称内部变量。 形式参数表中的变量也是一种内部变量 形式参数的定义 int max(int x,int y) { … }
函数的命名和参数 函数的命名规则 函数参数: 形式参数 (形参): 实际参数(实参): 在定义函数时,函数名后面括号中的变量名 在主调函数中调用一个函数,调用函数名后面括号中的参数(或表达式)
例5.2实际参数与形式参数 #include <stdio.h> void main( ) { int max(int x,int y); /*max函数的声明*/ int a,b,c; scanf("%d,%d",&a,&b); c=max(a,b); /*a,b为实际参数*/ printf("Max is %d",c); } int max(int x,int y) /*x,y为形式参数*/ int z; z=x>y?x:y; return z;
关于形参与实参的说明 形参只有在发生函数调用时才临时分配内存单元,调用结束后立即释放 定义函数时必须指定形参的类型 实参可以是常量、变量、表达式、数组名;形参只能是变量 实参与形参的数据传递是“值传递”,即单向传递 实参与形参的类型应相同或赋值兼容(复习2.7)
函数的返回值 返回值通过return语句获得。return语句的作用就是将被调用函数中的一个特定的值返回给主调函数。 一个函数可以没有return语句(void型的函数)。return语句后面的括号可要可不要。 return后面的值可以是常量,变量或者表达式。 int max(int x,int y) { return (x>y? x:y); }
函数的返回值 函数的类型就是返回值的类型,在定义函数时指定。如int max(int x,int y) 函数的类型和return中的表达式的类型相一致。 用“void”表示函数不返回值,为“无类型”或”空类型”。否则的话,函数可能返回一个不确定的值。 例:void printstar() { printf("* * * * * * * * * * * * * * * * \n"); }
一个好习惯 /* 函数功能:实现××××功能 函数参数:参数1,表示××××× 参数2,表示××××× 函数返回值: ××××× */ 返回值类型 函数名(形式参数表) { 函数体 return 表达式; }
函数示例 /* 函数功能: 计算平均值 函数入口参数: 整型x,操作数1 整型y,操作数2 函数返回值: 平均值,整型 */ 函数功能: 计算平均值 函数入口参数: 整型x,操作数1 整型y,操作数2 函数返回值: 平均值,整型 */ int Average(int x, int y) { int result; result = (x + y) / 2; return result; }
函数调用 函数调用的一般形式 提供的实际参数个数、类型、顺序应与定义时形参相同 函数调用过程: 函数名 (实际参数列表); 暂时中断主调函数的运行,转向被调函数 为被调函数的形参分配内存单元 计算主调函数实参的值,并传递给对应的形参 执行被调函数的函数体 释放被调函数形参的内存单元 返回主调函数,继续运行…
函数调用过程示例 void main() { int a = 12; int b = 24; int ave; int Average(int x, int y) { int result; result = (x + y) / 2; return result; } main() int a = 12; int b = 24; int ave; ave = Average(a, b); printf(……); 数据传递 执行顺序 void main() { int a = 12; int b = 24; int ave; ave = Average(a, b); printf("Average of %d and %d is %d.\n", a, b, ave); }
函数调用方法 1. 函数语句(后面加“;”) 如:printstar( ); 2. 函数表达式(出现在一个表达式中) 1. 函数语句(后面加“;”) 如:printstar( ); 2. 函数表达式(出现在一个表达式中) 如:c=2*max(a,b); 3. 函数参数(作为实参出现) 如:m=max(a,max(b,c)); 注:函数调用作为函数的参数,实质上也是函数表达式的一种形式
函数调用的条件 在一个函数中调用另一个函数需要哪些条件? 被调用的函数必须是已经存在的函数(库函数或用户自定义的函数) 使用库函数:文件中应用#include命令包含相关的头文件 使用自定义函数:如果函数定义在后,而调用在前的话,调用前必须先对函数进行声明
例 5.3 #include <stdio.h> void main( ) { float add(float x, float y); /*函数声明*/ float a,b,c; scanf("%f,%f",&a,&b); c=add(a,b); /* 函数调用*/ printf("sum is %f",c); } float add(float x,float y) /* 函数定义*/ float z; z=x+y; return z;
函数声明 “声明”的作用是将被调用函数的信息告之编译系统,帮助编译系统进行类型检查 函数声明又称为函数原型. 上例中的函数声明可以使用以下两种: float add(float x,float y); float add(float, float); 对被调函数不用进行声明的情况: 1. 被调函数的定义出现在主调函数之前 2. 在主调函数之前已经进行了声明。
例 5.4 #include <stdio.h> float add(float x,float y) /* 函数定义*/ { float z; z=x+y; return z; } void main( ) float a,b,c; scanf("%f,%f",&a,&b); c=add(a,b); /* 函数调用*/ printf("sum is %f",c);
例 5.5 #include <stdio.h> float add(float x,float y); /*函数声明*/ void main( ) { float a,b,c; scanf("%f,%f",&a,&b); c=add(a,b); /* 函数调用*/ printf("sum is %f",c); } float add(float x,float y) /* 函数定义*/ float z; z=x+y; return z;
函数的嵌套调用 C语言函数是互相平行的,独立的。 C语言允许函数嵌套调用(多层调用),即函数甲调用函数乙,而函数乙又调用了函数丙。 main() { … a(); } a 函数 b(); return; b函数 ① ③ ④ ⑤ ⑥ ⑦ ②
函数的递归调用 在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。 f函数 f1函数 f2函数 不能无终止的自身调用 递归调用应该有终止条件
例5.6 有5个人坐在一起,问第5个人岁数,他说比第4个人大2岁。问第4个人岁数,他说比第3个人大2岁。问第3个人,又说比第2个人大2岁。问第2个人,又说比第1个人大2岁。最后问第1个人,他说是10岁。请问第5个人多大? 这可以看作是一个递归问题. 令求第n个人年龄的函数是age(n)
求第5个人年龄的过程 age(5)=18 age(5)=age(4)+2 age(4)=age(3)+2 age(3) =age(2)+2 当n>1时,age(n)= age(n-1)+2; 当n=1时,age(n)=10
#include <stdio.h> int age(int n) { int c; if(n==1) c=10; else c=age(n-1)+2; return c; } void main( ) printf("%d",age(5));
函数调用过程 age函数共被调用5次 只有一次是主函数调用 其余4次是函数递归调用 age函数 n=5 age函数 n=4 age函数 main age(5) 输出age(5) c= age(4)+2 c= age(3)+2 c= age(2)+2 c= age(1)+2 c= 10 age(5)=18 age(4)=16 age(3)=14 age(2)=12 age(1)=10
作业 将密码机程序的加密和解密功能用函数来实现 使用函数的递归调用实现求n! 汉诺塔问题 来源于印度的一个古老的传说。开天辟地的神勃拉玛在一个庙里留下了三根金刚石的棒,第一根上面套着64个圆的金片,最大的一个在底下,其余一个比一个小,依次叠上去,庙里的众僧不倦地把它们一个个地从这根棒搬到另一根棒上,规定可利用中间的一根棒作为帮助,但每次只能搬一个,而且大的不能放在小的上面。
变量的类型
局部变量 局部变量 在函数内定义的变量是内部变量,它只在本函数范围内有效,称 “局部变量”。如: float f1(int a) /*函数f1*/ { int b,c; … } char f2(int x,int y) /*函数f2*/ {int i,j; void main( ) /*主函数*/ {int m,n; a,b,c 有效 x,y,i,j有效 m,n有效
关于局部变量 主函数main中定义的变量也只在主函数中有效。 不同函数中可以使用相同名字的变量,它们代表不同的对象,互不干扰。 形式参数也是局部变量。 更小的局部变量:在函数内部某个复合语句中定义变量,它们只在该复合语句中有效。
void main( ) { int a,b; … int c; c=a+b; } C在此范围内有效 a,b在此范围内有效
全局变量 在函数之外定义的变量称为外部变量或称全局变量 全局变量可为本文件中的所有函数共用 它的有效范围是从定义变量的位置开始到本源程序文件结束
int p=1,q=5; /*全局变量*/ float f1(int a) { int b,c; … } char c1,c2; void main( ) { int m,n; { int w; w=m+n; a,b,c有效 全局变量 的作用范围 全局变量 有效 p,q c1,c2 w m,n 有效
例5.7 #include <stdio.h> void GlobalPlusPlus(); main() { int global = 1; printf("Before GlobalPlusPlus(), it is %d\n", global); GlobalPlusPlus(); printf("After GlobalPlusPlus(), it is %d\n", global); } /* 函数功能: 对局部变量global加1,并打印加1之前与之后的值 函数入口参数: 无 函数返回值: 无 */ void GlobalPlusPlus() printf("Before ++, it is %d\n", global); global++; printf("After ++, it is %d\n", global); Before GlobalPlusPlus(), it is 1 Before ++, it is 1 After ++, it is 2 After GlobalPlusPlus(), it is 1
例5.8 #include <stdio.h> int global; /*定义全局变量*/ void GlobalPlusPlus(void); main() { global = 1; printf("Before GlobalPlusPlus(), it is %d\n", global); GlobalPlusPlus(); printf("After GlobalPlusPlus(), it is %d\n", global); } /* 函数功能: 对全局变量global加1,并打印加1之前与之后的值 函数入口参数: 无 函数返回值: 无 */ void GlobalPlusPlus(void) printf("Before ++, it is %d\n", global); global++; printf("After ++, it is %d\n", global); Before GlobalPlusPlus(), it is 1 Before ++, it is 1 After ++, it is 2 After GlobalPlusPlus(), it is 2
关于全局变量 全局变量的作用: 增加了函数之间数据联系的渠道(return只能带回一个值) 建议非必要时不要使用全局变量 如果在同一个源文件中,全局变量与局部变量同名,则局部变量的作用范围内,外部变量被“屏蔽”,即它不起作用
外部变量与局部变量同名 int a=3,b=5; void max(int a,int b) { int c; c=a>b?a:b; return(c); } void main( ) int a=8; printf("%d",max(a,b)); } 局部a 局部b 局部a 全局b
变量的存储类别 存储类别指数据在内存中存储的方式,即编译器为变量分配内存的方式,它决定变量的生存期。 动态存储 静态存储 根据需要临时分配存储空间,离开即释放 静态存储 在程序运行期间分配固定的存储空间,不释放 变量的作用域是从空间角度来看的。 静态、动态存储方式是从时间角度来看的
动态存储与静态存储 运行一个程序时,操作系统会为这个程序分配3种内存区域(3部分): (1)程序区(存放二进制程序代码) (2)静态存储区 (3)动态存储区 程序区 静态存储区 全局变量、 静态变量 动态存储区 形参、自动变量、函数调用的现场等
动态存储与静态存储 全局变量存放在静态存储区中,在程序开始时即分配存储空间,一直到程序结束时才释放。程序执行过程中它们占据固定的内存单元,不是动态地分配和释放。 在动态存储区中存放以下数据: a 函数形参 b 自动变量(普通的局部变量) c 函数调用时的现场保护和返回地址 在函数调用开始时分配在动态存储区中,并且在函数调用结束时释放这些空间。
变量存储类型 变量的存储类别包括两大类: 具体包含4种: 静态存储类 动态存储类。 auto(自动变量) static(静态变量) register(寄存器变量) extern(外部变量)
auto变量 函数中的局部变量,包括函数的形参,如果不专门声明,都是auto变量。通常,auto可以省略。 int f(int a) { int b; ....... }
static变量 有时候,我们希望函数中的局部变量的值,在函数调用结束后不消失而保留原值,即其占用的存储单元不释放,在下一次函数调用的时候继续使用这个值。这种变量就叫做局部静态变量,用static来声明。
例 5.9静态变量的值 #include <stdio.h> int f(int a) { auto int b=0; static c=3; b=b+1; c=c+1; return (a+b+c); } void main( ) int a=2,i; for(i=0;i<3;i++) printf(“%d”,f(a)); a+b+c 2+1+4 2+1+5 2+1+6
关于静态变量 静态局部变量在程序整个运行期间都不释放,它属于静态存储类别,在静态存储区中分配存储空间(注意和auto变量区别) 静态局部变量是在编译时赋初值,只赋初值一次,以后每次调用函数时不再重新赋初值而使用上次函数调用时保留的值 如果定义时不赋初值,编译器会自动对静态局部变量赋初值0 虽然静态局部变量在函数调用结束时不释放,但不能被其他函数引用。
静态变量与全局变量 静态变量和全局变量都是静态存储类型 程序区 静态存储区 动态存储区 自动初始化为0 从静态存储区分配,生存期为整个程序运行期间 但作用域不同 程序区 静态存储区 全局变量、 静态变量 动态存储区 形参、自动变量、函数调用的现场等
寄存器变量(register) 寄存器的存储速度远高于内存的存储速度(寄存器在cpu内部,读取的速度比内存高很多) register int i; 现代编译器有能力自动把普通变量优化为寄存器变量,并且可以忽略用户的指定,所以一般无需特别声明变量为register
用extern声明外部变量 外部变量就是全局变量, 用extern声明以扩展全局变量的作用域 int max(int x,int y) { int z; z=x>y?x:y; return(z); } main( ) extern A,B; /*外部变量声明*/ printf("%d",max(A,B)); int A=13,B=-8; /*定义全局变量*/
用static和extern定义函数 根据函数能否被其他源文件调用,将函数分为外部函数和内部函数 外部函数:[extern] int fun(int a,int b) 那么这个函数可以被其他源文件所调用。 在需要调用此函数的源文件中,首先用extern对这个函数进行声明,表明这个函数是在其他文件中定义的。 内部函数:当一个函数只能被本文件中其他函数调用,这个函数称为内部函数,定义时使用static。 static int fun(int a,int b)
模块化程序设计方法 什么时候需要模块化? 要尽可能复用其它人的现成模块。 某一功能,如果重复实现3遍以上,即应考虑模块化,将它写成通用函数,并向小组成员发布 要尽可能复用其它人的现成模块。
模块化程序设计方法 功能分解 模块分解的原则 设计好模块接口 自顶向下、逐步细化的过程 保证模块的相对独立性 模块的实现细节对外不可见 高聚合、低耦合 模块的实现细节对外不可见 外部:关心做什么 内部:关心怎么做 设计好模块接口 接口是指罗列出一个模块的所有的与外部打交道的变量等 定义好后不要轻易改动 在模块开头(文件的开头)进行函数声明
函数设计的原则 函数的功能要单一,不要设计多用途的函数 函数的规模要小,尽量控制在50行代码以内 参数和返回值的规则 1986年IBM在OS/360的研究结果:大多数有错误的函数都大于500行 1991年对148,000行代码的研究表明:小于143行的函数比更长的函数更容易维护 参数和返回值的规则 参数要书写完整,不要省略 没有参数和返回值时,用void填充 每个函数只有一个入口和一个出口,尽量不使用全局变量 尽量少用静态局部变量,以避免使函数具有“记忆”功能
模块和链接 优点: 当一个文件的代码被修改后,不必对所有程序重新编译,从而节省了程序的编译时间。 使程序更宜于维护,给多个程序员共同编制一个大型项目的代码提供了方便手段。
这一章我们学习了 函数的定义、调用 变量的作用域、存储类别 进一步理解程序设计原则