1、语言函数与程序结构语言函数与程序结构教学内容 4.1 概述概述4.1 概述概述(P73)问题提出问题提出 前面的程序中前面的程序中,都用到了主函数都用到了主函数“main”及及 C语言提供的输入输出库语言提供的输入输出库函数函数 scanf 和和 priintf 等函数等函数,目前除目前除了调用库函数实现某些功能外了调用库函数实现某些功能外,所有所有的练习中程序功能均在主函数中完的练习中程序功能均在主函数中完成成,当问题有一定规模和复杂度时不当问题有一定规模和复杂度时不可避免地会出现如下问题可避免地会出现如下问题 程序越来越长程序越来越长,难于理解且可读难于理解且可读性下降。性下降。重复代码增
2、多重复代码增多,某段程序可能被执某段程序可能被执行多次。行多次。某一问题中的代码某一问题中的代码,无法在其他同无法在其他同类问题中再用类问题中再用,必须重复原来设计编必须重复原来设计编码的过程。码的过程。程序各部分之间联系复杂、保持程序各部分之间联系复杂、保持重用代码段的一致性等原因导致程重用代码段的一致性等原因导致程序调试难度加大。序调试难度加大。4.1 概述概述 4.1 概述概述 函数可以被多次调用,从而减少程序的长度。增加程序的可读性。程序的模块化、结构化模块化、结构化更强。C语言中,函数名是上述封装体的名称。函数定义是按照规定形式对函数的描述。函数调用是要求执行函数的描述。一个实用的C
3、语言源程序总是由许多函数组成,这些函数多数都是根据实际任务由用户来编写的,在这些函数中,可以调用C语言提供的库函数,也可调用用户自己写的或他人编写的函数。4.1 概述概述 C程序总是从主函数 main 开始执行,到主函数的最后一个“”处结束。在 main 中,调用库函数或自己定义的函数时,控制权转移到函数,主程序等待,待函数执行完毕后,控制返回到调用处,主函数继续执行。其他函数中调用函数的过程与此相同。从C语言函数来讲,一方面要掌握库函数的调用方法,尽可能地熟悉系统提供的常用库函数的功能,尽量使用库函数实现程序功能。另一方面,库函数毕竟不能解决所有的问题,需要掌握自己定义函数的方法。4.1 概
4、述概述 一种观点是从函数外部调用者的角度考虑一种观点是从函数外部调用者的角度考虑怎样使用和设计函数,不论是库函数还是自定义函数,我们只关心函数如何使用,实现什么功能、函数名字是什么、有几个参数、类型是什么、返回什么值,而不关心函数内部的实现细节,也没有必要关心。从这个层次上考虑问题,可以摆脱细节干扰、把握总体和全局应该具有的功能和结构。另一种观点是从函数内部实现者的角度另一种观点是从函数内部实现者的角度,定义函数实现的功能,要考虑函数启动时需外部提供什么数据及其类型、函数如何工作、如何得到结果、何时结束、如何返回结果,而不关心外部什么地方调用这个函数,调用时具体参数是什么。我们在函数内部定义的
5、计算功能是具有一般性的抽象计算功能,只有函数被调用的时候,才具体化地解决一个实际问题。模块化程序设计 4.1 概述概述l基本思想 将一个大的程序按功能分割成一些小模块。l特点 Y 各模块相对独立、功能单一、结构清晰、接口简单。Y 控制了程序设计的复杂性。Y 提高元件的可靠性。Y 缩短开发周期。Y 避免程序开发的重复劳动。Y 易于维护和功能扩充。l开发方法 自上向下,逐步分解,分而治之。4.2 库函数库函数4.2 库函数库函数(P74)C语言非常简洁,程序所需的许多东西通过函数方式提供。掌握C语言的一个重要方面,就是要熟悉库函数的使用。ANSI C标准对过去各种C语言系统的函数进行了分析,并将其
6、规范化,它将一批最常用的功能总结出来,定义了C语言的标准库、每种C语言系统,都按照这个标准提供了一批标准库函数,还根据实际运行环境提供了扩充函数库。写C程序时可直接调用函数,不必关心函数是如何实现的,这样可节省大量的时间和精力。使用库函数,要学会使用联机帮助或系统的有关手册。参考 P234 列出了常用的库函数。auto(自动)变量采用这种形式后,如果这个文件第二次被包含时,由于 MY_HEAD_FILE 已经有了定义,#if 和#endif之间的所有东西都将被丢掉。不鼓励使用这种用法,原因是在这种情况下C语言编译器无法得到关于函数参数和参数类型的信息。/*定义局部变量 i,j*/return(
7、x+y);int f1(int x)无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。math.概念 函数的递归调用是指,一个函数在它的函7 多文件程序的组织和调试方法 hanoi(n1,two,one,three);解决的方法是,在一个文件中定义全局变量,在其他要引用的文件中用 extern 对变量进行说明。float add(float x,float y)/*x,y 为实形参*/请读者在第 5 章的内容学习完毕之后,再回到本例仔细体会本例所示例的结构。/*a 自动进行初始化,为 0*/x*=2;4.2 库函数库函数 4.2 库函数库函数 表达式中调
8、用。函数是表达式的一个运算对象,如 y=z*sin(x)+0.5 这种调用方式,通常需要使用函数的返回值进行计算;作为独立语句。可看作表达式语句如printf(%dn”,a);在 Turbo C的编辑器中,将光标置于库函数的任意一个字符上,按下 Ctrl+F1,可得到关于此库函数的定义信息。调用库函数时,要注意按照库函数的参数类型(形参)要求,给出调用的参数(实参)。实参可以是表达式,函数调用的第一步就是计算实参的值,然后传给形参变量。4.3 函数函数定义定义4.3 函数函数定义定义(P75)函数类型 函数名(类型名 形参1,类型名 形参2)说明部分 语句部分 函数名、形参是用户命名的标识符,
9、同一程序中,函数名必须唯一,形参名在同一函数中唯一即可,可以与其他函数中的变量同名。C语言规定,不能在函数的内部再定义函数(即嵌套定义)。所有函数都是互相平行独立的。省略返回类型名时,缺省值为 int。函数体 4.3 函数函数定义定义 函数名后括号内的参数叫形参,形参必须用括号括起来,可有多个形参,也可以一个也没有。函数定义的一对 中可以为空,这样的函数什么也不做。函数体中说明部分的变量,只在函数体内有效,并在进入函数时为变量开辟单元,退出时释放分配的单元。函数体中说明部分的变量与其他函数体中的变量互不相关。函数头部构成函数内外部之间的界面。函数接口定义清楚了,函数的定义和使用完全可以由不同人
10、来完成。函数允许递归调用,即在函数中可直接或间接地调用自己。函数值通过 return 语句返回。例 无参函数 printstar()printf(*n);或 printstar(void)printf(*n);例 有参函数(现代风格)int max(int x,int y)int z;z=xy?x:y;return(z);例 空函数 dummy()【例4.1】写一个函数,求两个双精度浮点数的和。double add(double a,double b)double s;s=a+b;return s;不能省略例:int max(int x,int y)int z;z=xy?x:y;return(
11、z);4.3 函数函数定义定义 在老版本 C语言中,参数类型说明允许放在函数说明部分的第 2 行单独指定。其格式如下:函数类型 函数名(形参表)形参类型说明 说明部分 语句部分 例如:求两个双精度浮点数的和,写成传统风格的有参函数如下:double add(a,b)double a,b;/*参数类型说明*/double s;s=a+b;return s;4.3 函数函数定义定义uvoid 型函数 表示该函数没有值返回,调用这类函数的惟一的方法就是作为独立的语句。例 无返回值函数 void swap(int x,int y)int temp;temp=x;x=y;y=temp;void prin
12、tstar()printf(*);main()int a;a=printstar();printf(%d,a);编译错误!例:函数返回值类型转换 main()float a,b;int c;scanf(%f,%f,&a,&b);c=max(a,b);printf(Max is%dn,c);max(float x,float y)float z;z=xy?x:y;return(z);int 为了使程序具有良好的可读性为了使程序具有良好的可读性并减少出错并减少出错,凡不要求返回值的函数凡不要求返回值的函数都应定义为空类型都应定义为空类型;即使函数类型即使函数类型为整型为整型,也不使用系统的缺省处理
13、。也不使用系统的缺省处理。sub();7 多文件程序的组织和调试方法 参考 P234 列出了常用的库函数。printf(main:a=%d,b=%dn,a,b);从而可避免因不慎赋值所导致的错误影响到其他函数。printf(file1:%dn,n);else hanoi(n1,one,three,two);作用域 只在函数或复合语句体(复合体)范围内有效,在此函数或复合语句体外不能使用这些变量。int x;/*定义全局变量*/大的源程序在处理中存在的问题void move(char getone,char putone)printf(main:a=%d,b=%dn,a,b);5 调用函数和被调
14、函数的数据传递语句部分(2)x=10,y=20从而可避免因不慎赋值所导致的错误影响到其他函数。函数体中说明部分的变量,只在函数体内有效,并在进入函数时为变量开辟单元,退出时释放分配的单元。printf(x=%dn,x);程序各部分之间联系复杂、保持重用代码段的一致性等原因导致程序调试难度加大。4.4 函数的调用和函数说明函数的调用和函数说明 4.4 函数的调用和函数说明函数的调用和函数说明(P76)l 函数调用的形式 函数名(实参表);l 函数调用方式。与库函数的调用类似,有以下两种调用方式:函数语句。只进行某些操作,不返回函数值。以函数作为独立的语句,末尾一定要加“;”号。函数表达式。函数作
15、为表达式的一项,出现在表达式中,以函数返回值参与表达式的运算。这种方式要求函数是有返回值的。如:函数表达式 例 4.l 定义的函数 add,允许下列的调用方式:y=add(3.0,4.0);for(y=0,i=1;i 0 printf(“%d”,add(a,b);/*函数为参数*/函数语句而对于前面定义的函数 printstar 只能用函数语句调用方式:printstar();4.4 函数的调用和函数说明函数的调用和函数说明 l 说明Y 函数名后面的列表内的参数叫实参。Y 调用的函数名字必须与定义的函数名字完全一致。Y 实参个数与形参个数应一致,类型也应统一;如不匹配,系统进行自动转换。不兼容
16、的赋值转换(某些类型之间不能进行赋值,如指针和浮点类型之间)并不给出错误信息,程序继续运行,但结果不正确,这点应特别注意。Y 函数可以嵌套调用。如 a 函数调用 b 函数,b 函数中又调用 C 函数。(但数不许嵌套定义)Y 实参表求值顺序,因系统而定(Turbo C 自右向左)如:参数求值顺序main()int i=2,p;p=f(i,+i);printf(p=%d,p);int f(int a,int b)int c;if(ab)c=1;else if(a=b)c=0;else c=-1;return(c);运行结果:p=0p=f(i,i+);运行结果:p=1 4.4 函数的调用和函数说明函
17、数的调用和函数说明 l【例 4.2】写程序,从键盘读入两个浮点数,用例 4.1 中定义的函数求两个双精度浮点数的和。Y 程序#includedouble add(double,double)/*原型说明*/main()double x,y,z;scanf(%lf%lf,&x,&y);z=add(x,y)printf(z=%lf,z);double add(double a,double b)return(a+b);4.4 函数的调用和函数说明函数的调用和函数说明 l 函数的原型说明概念 C语言规定,函数必须先定义,后调用。但实际写程序时,由于函数之间可能互相调用,也很难保证这种关系总是成立。解
18、决这种问题的方法是在调用之前对函数进行说明,这种方法称为函数的原型说明。l 对被调用函数要求 Y 必须是已存在的函数Y 库函数:#include Y 用户自定义函数:函数类型说明 4.4 函数的调用和函数说明函数的调用和函数说明 l 函数说明一般形式 l 函数类型 函数名(形参类型 形参名,);l 或 函数类型 函数名();l 函数说明作用 l 告诉编译系统函数类型、参数个数及类型,以便检验,因此参数列表中,只需类型名即可。如形参名可以是任意的用户标识符,不一定要和函数定义的形参名相同。l 说明l函数原型说明的形式可理解为函数定义的首都加分号“;”l函数说明位置程序的数据说明部分(函数内或外)
19、。p78l函数定义与函数说明不同。函数说明举例main()float add(float,float);/*函数说明*/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);4.4 函数的调用和函数说明函数的调用和函数说明 l 可以省去对被调用函数的说明两种情况 被调用函数定义出现在主调函数之前。若函数返值是 char 或 int 型,系统自动按 int型处理。不鼓励使用这种用法,原因是在这种情况下C语言编译器无法得到关于函数参
20、数和参数类型的信息。l 函数原型说明给程序设计带来了极大的灵活性 在一个比较复杂的设计中,总是包含很多函数,通过使用函数原型说明,可以随意安排函数定义的位置,如既可以把主函数写在所有其他函数的后面,也可以把主函数写在所有其他函数的前面。例:被调函数出现在主调函数之前,不必函数说明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 is%f,c);例:int 型函数可不作函数说明main()float a,b;int c;scanf
21、(%f,%f,&a,&b);c=max(a,b);printf(Max is%dn,c);max(float x,float y)return(xy?x:y);错误!但没有错误信息的报告!改为:c=max(a)4.5 调用函数和被调函数的数据传递调用函数和被调函数的数据传递 4.5 调用函数和被调函数的数据传递调用函数和被调函数的数据传递l 形参 定义函数时函数名后面括号中的变量名。只能在该函数体内使用。l 实参 调用函数时函数名后面括号中的表达式。l 作用 实现数据传送。实参和形参之间进行数据传递。return 语句把函数值返回调用函数。通过全局变量进行数据传递(稍后讨论)main()flo
22、at add(float,float);/*函数说明*/float a=5,b=8,c;c=add(a,b);/*a,b 为实参*/printf(sum is%f,c);float add(float x,float y)/*x,y 为实形参*/return(x+y);4.5 调用函数和被调函数的数据传递调用函数和被调函数的数据传递 l 值传递方式 函数调用时,为形参分配单元,并将实参的值复制到形参中;调用结束,形参单元被释放,实参单元仍保留并维持原值l 值传递特点 Y 形参与实参占用不同的内存单元,即使同名也互不影响。Y 单向传递,即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参
23、。l 实参与形参结合的本质 C语言的函数调用机制是很简单的,值传递是C语言实参与形参结合的惟一方法,后面的实参的其他情形,离不开值传递这个本质。值传递!所谓外部,是指写在源程序文件表层的定义和说明,这种说法是相对于出现在函数体里面的定义或说明而言的。c=add(a+b,3);printf(a=%d n,a);fact(1)char f2(int x,int y)7 变量的作用域和生命期 在进行实际项目开发时,程序开发者通常临时写一些用于调试的主程序,和这些程序文件进行连接,以发现其中的问题。不同函数中同名变量,占不同内存单元,实例采用这种形式后,如果这个文件第二次被包含时,由于 MY_HEAD
24、_FILE 已经有了定义,#if 和#endif之间的所有东西都将被丢掉。解决这种问题的方法是在调用之前对函数进行说明,这种方法称为函数的原型说明。本例中,宏的概念在第 5 章才会介绍,这里请注意的是解决重复包含问题的程序语句结构问题。for(y=0,i=1;i 0 调试时,个别地方的修改引起整个文件重新编译;实例 int p=1,q=5;/*外部变量的定义*/*a 自动进行初始化,为 0*/int max(int x,int y,int z);在复合语句中也可定义局部变量。概念 存储类别指的是变量在内存中的存储位置,有静态和动态两种存储类别。printf(“%d;%d,x,y);printf
25、(file1:%dn,n);4.5 调用函数和被调函数的数据传递调用函数和被调函数的数据传递 l 说明 Y 实参可以是常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。Y 形参必须指定类型;Y 形参与实参类型一致,个数相同;Y 若形参与实参类型不一致,自动按形参类型转换 函数调用转换;Y 形参变量只有在被调用时,才分配内存单元;调用结束时,即刻释放所分配的内存单元。因此,形参只有在该函数内有效。调用结束,返回后,则不能再使用该形参变量。float add(float x,float y)return(x+y);main()fl
26、oat a=5,b=8,c;c=add(a+b,3);printf(sum is%f,c);4.5 调用函数和被调函数的数据传递调用函数和被调函数的数据传递 l【例4.3】以下程序企图通过调用函数 swap,交换主函数中变量 x 和 y 中的数据。请观察程序的输出结果。#include main()int x=10,y=20;printf(1)x=%d,ty=%dn,x,y);swap(x,y);printf(2)x=%d,ty=%dn,x,y);swap(int a,int b)int temp;temp=a;a=b;b=temp;1020 x:y:调用前:调用:1020 x:y:1020a
27、:b:temp1020 x:y:2010a:b:swap:1020 x:y:调用结束:运行结果:(1)x=10,y=20(2)x=10,y=20 4.5 调用函数和被调函数的数据传递调用函数和被调函数的数据传递 l【例4.4】设计程序,判断一个整数是否为素数。#include int isprime(int);main()int;printf(Input an integer number:);scanf(%d,&x);if (isprime(x)printf(%d is a prime n,x);else printf(%d is not a prime n,x);isprime(int a
28、)/*若 a 是素数,返回 1;否则返回 0*/int i;for(i=2;i=sqrt(double)a);i+)if(a%i=0)return 0;return 1;main()调用函数调用函数 a后续语句后续语句结束结束a 函数函数调用函数调用函数 b后续语句后续语句returnb 函数函数return 4.6 函数的嵌套调用与函数的嵌套调用与递归函数递归函数 4.6 函数的嵌套调用与函数的嵌套调用与递归递归调用调用l 概念概念 嵌套调用是指,在执行被调用函数时,被调用函数又调用了其它函数。(但定义不可嵌套)l 关系图关系图 4.6 函数的嵌套调用与函数的嵌套调用与递归函数递归函数 l
29、实例实例 求三个数中最大数和最小数的差值#include int dif(int x,int y,int z);int max(int x,int y,int z);int min(int x,int y,int z);main()int a,b,c,d;scanf(%d%d%d,&a,&b,&c);d=dif(a,b,c);printf(Max-Min=%dn,d);int dif(int x,int y,int z)return max(x,y,z)-min(x,y,z);int min(int x,int y,int z)int r;r=xy?x:y;return(ry?x:y;retu
30、rn(rz?r:z);4.6 函数的嵌套调用与函数的嵌套调用与递归函数递归函数(P80)l 概念概念 函数的递归调用是指,一个函数在它的函 数体内,直接或间接地调用它自身。l 说明说明在递归调用中,调用函数又是被调用函数,执行递归函数将反复调用其自身。每调用一次就进入新的一层并在内存堆栈区分配空间,用于存放函数变量、返回值等信息,所以递归次数过多,可能引起堆栈溢出。系统对递归函数的自调用次数没有限制。为了防止递归调用无终止地进行,必须在函数内有终止递归调用的手段。常用的办法是加条件判断,满足条件后就不再作递归调用。int f(int x)int y,z;z=f(y);return(2*z);i
31、nt f1(int x)int y,z;z=f2(y);return(2*z);int f2(int t)int a,c;c=f1(a);return(3+c);直接调用自身:f()调调用用 f()间接调用自身:调调用用f2调调用用f1f1()f2()4.6 函数的嵌套调用与函数的嵌套调用与递归函数递归函数 C提供递归函数,有其深远的数学和现实背景。首先我们来看两个数学中的递归定义实例:定义的右边使用了定义本身。如左式描述的计算方法中,把一个数的阶乘用比它小1的数的阶乘来定义,这样,计算过程最终归结到 0 的阶乘,如果C语言支持用同样的方法定义计算过程,类似问题的求解将非常简单。C语言确实提供
32、了递归地调用函数的机制。0n1)!(nn0n1n!当当0nee0n1e1nn当当 4.6 函数的嵌套调用与函数的嵌套调用与递归函数递归函数 所有的递归问题都可用非递归的方法解决。递归程序设计的关键是抽象出递归模型,要把递归问题转化为一个新问题,且这个新问题的解法仍与原来的解法相同,只是所处理的对象规模有规律地递增和递减。一旦得到这个模型,使用递归算法写出的程序在算法的可读性和正确性方面都比同一问题的非递归程序更好。采用递归算法的程序的问题是尽管代码比非递归程序紧凑,但其程序执行的效率不如非递归程序。另一方面,一些比较复杂的递归问题用非递归方法实现,往往使程序变得十分复杂而且难以理解。一个问题是
33、否使用递归方法解决,需要在效率和简洁易懂的程序代码之间取得平衡。4.6 函数的嵌套调用与函数的嵌套调用与递归函数递归函数 l【例 4.5】设计阶乘 n!的递归函数。Y 分析分析 函数的形参只有一个整型数。由于阶乘的值随着参数的增长而增长的速度非常快,返回值类型应为长整型较好。Y 程序程序Y 调用过程调用过程(设n为4)fact(4)4*fact(3)fact(3)3*fact(2)fact(2)2*fact(1)fact(1)1*fact(0)fact(0)1#include long fact(int n)int f;if (n0)printf(n%cn,getone,putone);Yvo
34、id hanoi(int n,char one,char two,char three)Y if (n=1)move(one,three);Y else hanoi(n1,one,three,two);Y move(one,three);Y hanoi(n1,two,one,three);Y Y Ymain()Y int m;Y printf(Enter the number of disks:);Y scanf(%d,&m);Y printf(The steps to moving%3d disks:n,m);Y hanoi(m,A,B,C);Y 运行结果:Enter the number
35、of diskes:3The step to moving 3 diskes from A to C:a-ca-bc-ba-cb-aHanoi(汉诺)塔问题 4.6 递归函数递归函数 Hanoi(汉诺)塔问题,是经典的递归问题,研究过递归问题的人,可能没有几个不知道它,传说它是古代印度布拉玛庙的僧侣们玩的一种游戏,游戏的装置是一块铜板上面有三根杆 A、B、C,最左边杆自上而下、由小到大串上 64 个金盘构成一个塔,游戏的目标是把左边杆 A 上 的金盘全部移到最右边的杆 C 上,条件是一次只能移动一个盘,并且不允许大盘在小盘上面。ABC 4.7 变量的作用域和生命期变量的作用域和生命期 4.7
36、变量的作用域和生命期变量的作用域和生命期l 大的源程序在处理中存在的问题大的源程序在处理中存在的问题Y 编辑时,由于屏幕只显示文件的很小一部分翻阅和查找费时,处理起来很麻烦;Y 调试时,个别地方的修改引起整个文件重新编译;Y 开发过程中,程序先完成的部分与正开发的部分混在一个源程序中,调试新的部分时有可能无意中修改了已调试好的部分;Y 更进一步,一个大的程序有可能是许多人共同合作,无法让他们使用同一个源程序。4.7 变量的作用域和生命期变量的作用域和生命期 l 合理的程序结构合理的程序结构 解决大的源程序存在的问题,必须把程序结构组织好。合理的结构应该是将程序组织在若干个长度合适的源程序文件内
37、,对每个源程序单独编译、调试,最后再将所有编译好的目标模块连接起来。这些源程序文件本质上是处理同一问题的不同片段,它们之间或多或少会发生某些关系,比如会有公用的变量、互相调用的函数等,本节讨论的问题是理解和实现多文件结构程序的基础。4.7 变量的作用域和生命期变量的作用域和生命期 l 变量使用中的几个问题变量使用中的几个问题Y 在C语言里,变量必须先定义后使用,但定义语句应该放在什么地方?Y 在程序中,一个定义了的变量是否随处可用?(这些问题都涉及变量的作用域)Y 经过赋值的变量是否在程序运行期间总能保存其值?(这就涉及到变量的生存期)Y 当一个程序的若干函数分别存放于不同的源程序文件中时有什
38、么限制?本节将讨论C语言对这些问题的规定。4.7 变量的作用域和生命期变量的作用域和生命期 l“定义定义”和和“说明说明”的严格区别的严格区别 “定义”是指给变量分配确定的存储单元,“说明”只是说明变量的性质,并不分配存储空间。一个C语言的源程序文件由一系列的外部定义和外部说明构成。所谓外部,是指写在源程序文件表层的定义和说明,这种说法是相对于出现在函数体里面的定义或说明而言的。函数定义是一种外部定义。写在外层(不在任何函数体内)。一个外部定义或说明总是从它在源程序文件中的出现位置开始起作用,直到这个源文件结束。C语言规定函数只能在函数外部定义,不许在函数的内部定义另一个函数,这样,所有函数都
39、定义在程序的表层,整个程序的结构比较简单。4.7 变量的作用域和生命期变量的作用域和生命期 l 概述概述Y 定义一个变量的意义定义一个变量的意义 给变量定义一个名字,在程序中通过名字可以操作该变量的存储空间,进行取值和赋值;定义了该变量存储空间中数据的存在形式、范围和可以进行的运算;程序中哪些地方可以使用该变量;(作用域)程序运行过程中,该变量的存储空间什么时候分配和撤销。(存在期)后两点是这节要讨论的作用域和存在期。4.7 变量的作用域和生命期变量的作用域和生命期 Y 变量的作用域变量的作用域 变量的作用域是源程序中的某一部分,在这个范围里,变量定义是有效的,可以使用该变量的名字进行与该变量
40、有关的操作,每个变量都有一个确定的“作用域”,由变量定义出现的位置确定。作用域是变量的作用范围,一个定义的作用域是源程序中的一段,可以从源程序正文中把有关的程序段划分出来,因此作用域是静静态概念态概念,与程序的执行过程无关。4.7 变量的作用域和生命期变量的作用域和生命期 Y 变量的存在期变量的存在期 C语言对变量的操作,实际上是对分配给变量的存储单元的操作。事实上并不是所有的变量都是在程序一开始就建立好,直至程序结束才撤销,程序里各种变量存在的时间也可能有所不同。C语言把变量在程序中存在的那段时期称为该变量的“存在期”。存在期是动态概念动态概念,讲的是程序执行的一段时期,在一个变量的存在期里
41、,它所占的存储单元一直保留,只要不对变量重新赋值,单元中的值就保持不变。4.7 变量的作用域和生命期变量的作用域和生命期 Y 存储空间存储空间的组成的组成 内存中供程序使用的存储空间分为三部分:程序区程序区 存放程序代码。静态存储区静态存储区 静态存储区中的数据,在程序执行过程中,占据固定的存储单元,这些存储单元在程序开始时分配,在程序结束时释放。动态存储区动态存储区 动态存储区存放程序运行中动态地进行分配和释放的数据,包括函数的形参、自动变量(随后讨论),调用函数时的现场保护数据和返回地址等。float a=5,b=8,c;char f2(int x,int y)多文件程序的运行调试方法7
42、变量的作用域和 /*原来的头文件内容*/int m,n;float a,b,c;将多文件的程序组织起来的方法swap(x,y);printf(p=%d,p);else printf(%d is not a prime n,x);int i;当一个程序的若干函数分别存放于不同的源程序文件中时有什么限制?int n,y;分析 需要一个计数变量,每次调用函数,将该变量的值加 1,发现到 10 时,输出信息;return(rz?r:z);7 变量的作用域和生命期 存放程序代码。作用是通知编译程序该变量是一个已在外部定义了的全局变量,已分配了存储单元,不需再为它开辟存储单元。Enter the numb
43、er of diskes:3 4.7 变量的作用域和生命期变量的作用域和生命期 Y 存储类别存储类别 概念概念 存储类别指的是变量在内存中的存储位置,有静态和动态两种存储类别。C语言包含四种与存储类别有关的说明符语言包含四种与存储类别有关的说明符 auto 自动 static 静态 register 寄存器 extern 外部 变量定义格式变量定义格式 存储类别 数据类型 变量表;作用作用 根据变量的存储类别可以知道变量的作用域和存在期。对存储类别进行说明的例子:auto int i,j;static int n;register k;extern int m;4.7 变量的作用域和生命期变量
44、的作用域和生命期 l 局部变量的作用域和生命期局部变量的作用域和生命期Y 概念概念 局部变量是在一个函数内部或复合语句内部定义的变量。Y 作用域作用域 只在函数或复合语句体(复合体)范围内有效,在此函数或复合语句体外不能使用这些变量。实例Y 说明说明 main 中定义的变量只在 main 中有效;不同函数中同名变量,占不同内存单元,实例 形参属于局部变量。在复合语句中也可定义局部变量。Y 存储类别存储类别 局部变量可以使用 auto(自动)、static(静态)和 register(寄存器)说明符。4.7 变量的作用域和生命期变量的作用域和生命期 auto(自动)变量定义 局部变量定义时使用
45、auto 说明符或没有指定存储类别,系统就认为所定义的变量具有自动类别。空间的分配 系统对自动变量是动态分配存储空间的,数据存储在动态存储区中,像函数中的形参和在函数中定义的变量,在调用函数时系统会给它们分配存储空间,在函数调用结束时就自动释放这些存储空间,因此,这类局部变量称为自动变量。auto(自动)变量的说明 自动变量是使用最多的局部变量,auto 常省略,我们前面使用的变量并没有声明为 auto,都隐含指定为自动变量。如:int k;和 auto int k;是等价的。4.7 变量的作用域和生命期变量的作用域和生命期 局部变量的定义必须放在函数体或复合体中所有可执行语句前。自动变量的作
46、用域是从定义的位置起,到函数体或复合体结束为止。它的存储单元在进入这些局部变量所在的函数体(或复合体)时生成,退出其所在函数体(或复合体)时消失,这就是自动变量的存在期。当再次进入函数体(或复合体)时,系统将为它们另行分配存储单元,因此,变量的值不可能被保留。使用自动变量的优点是变量局部存于函数中,可在各函数之间造成信息隔离,不同函数中使用了同名变量也不会相互影响。从而可避免因不慎赋值所导致的错误影响到其他函数。4.7 变量的作用域和生命期变量的作用域和生命期【例 4.11】给出下列程序的运行结果#include int fun();main()int a=5;/*a 是自动变量*/a=fun
47、(a)printf(a=%d n,a);int fun(int x)/*x 是自动变量*/if (x0)int k;/*k 是自动变量*/k=2*x;return k;else return x;x 的作用范围a的作用范围k的作用范围运行结果:a=10 4.7 变量的作用域和生命期变量的作用域和生命期 register(寄存器)变量定义 寄存器变量也是自动变量,它与自动变量的区别仅在于空间的分配不同。空间的分配 建议编译程序将变量的值保留在 CPU 的寄存器中,而不像一般变量那样,占用内存单元。作用 程序运行时,访问寄存器的速度要比访问内存快得多,将频繁使用的少数变量指定为 register
48、变量,有助于提高程序的运行速度。说明 编写程序时使用寄存器变量只是对编写程序的一种建议,而不是强制性的。4.7 变量的作用域和生命期变量的作用域和生命期 CPU中的寄存器数目有限,在一个函数中允许使用寄存器变量的数目与 CPU 的类型和使用的 C 编译程序有关,当没有足够的寄存器来存放指定的变量,或编译程序认为指定的变量不适合放在寄存器中时,指定变量自动按自动变量处理。寄存器变量放在寄存器内而不是内存中,所以不能进行求地址的运算。寄存器变量的说明应尽量靠近其使用的地方,用完后尽快释放,以提高寄存器的利用效率。比如,常把寄存器变量的说明放在复合语句中来实现。4.7 变量的作用域和生命期变量的作用
49、域和生命期【例4.12】用寄存器变量设计实现 xn 的函数。分析 本题需要一个进行累乘操作的循环,循环的次数受指数 n 的控制,循环变量的初始值是指数函数的底 x,设 x、n 和函数的返回值类型均为 int,函数名为 power,程序 int power(int x,register int n)register int p;for(p=x;n;n)p=p*x;return p;说明 实际实现时,xn 的值增加很快,可考虑使用长整型数表示 x 和函数的返回值。4.7 变量的作用域和生命期变量的作用域和生命期 static(静态)变量定义 在函数体(或复合体)内部用 static 说明的变量,称
50、静态局部变量。空间的分配 静态局部变量在静态存储区占据永久性的存储单元,函数退出后下次再进入该函数,静态局部变量仍使用原来的存储单元,所以它的值在函数调用后不消失而保留下来,下次调用时,该变量的值是上次调用结束时的值。作用 静态局部变量的作用域和自动变量、寄存器变量一样,但其存在期与它们有本质的区别,要一直延长到程序运行结束。4.7 变量的作用域和生命期变量的作用域和生命期 补充实例 局部静态变量值具有可继承性。main()void increment(void);increment();increment();increment();void increment(void)int x=0;x