*,单击此处编辑母版标题样式,单击此处编辑母版文本样式,第二级,第三级,第四级,第五级,*,*,*,第六章 函数与编译预处理,6.1,模块化程序设计与函数,6.2 函数的定义与调用,6.3,函数的递归调用,6.4 变量的作用域与存取方式,6.5,编译预处理,1,第六章 函数与编译预处理6.1 模块化程序设计与函数1,6.1,模块化程序设计与函数,在设计较复杂的程序时,我们一般采用的方法是:把问题分成几个部分,每部分又可分成更细的若干小部分,逐步细化,直至分解成很容易求解的小问题。这样的话,原来问题的解就可以用这些小问题来表示。,2,6.1模块化程序设计与函数 在设计较复杂的程序时,基本概念,基本模块,模块,模块,模块,模块,模块,模块,模块,模块,模块,2024/11/19,3,基本概念基本模块模块模块模块模块模块模块模块模块模块2023,模块与函数,C,语言程序由基本语句和函数组成,每个函数可完成相对独立的任务,依一定的规则调用这些函数,就组成了解决某个特定问题的程序。,把大任务分解成若干功能模块,用多个函数来实现这些功能模块。通过函数的调用来实现完成大任务的全部功能。,任务、模块与函数的关系:一个大任务分成多个功能模块,功能模块则由一个或多函数实现。,模块化的程序设计是靠设计函数和调用函数实现的,。,4,模块与函数C语言程序由基本语句和函数组成,每个函数可完成相对,例:分数排序,任务,:输入三个数,从大到小的顺序的输出。如果大于等于85,在该数后面输出,A,,小于85且大于等于70,则输出,B,,小于70且大于等于60,输出,C,,如果小于60,则输出,D。,思路:,scanf(),输入分数另建一个排序函数判断并输出等级函数打印分数及等级的函数,虽然也可以由一个主函数来完成,但这样做可读性及操作性会更好。,5,例:分数排序任务:输入三个数,从大到小的顺序的输出。如果大,模块设计的原则,模块独立,规模适当,层次分明,功能专一,6,模块设计的原则模块独立规模适当层次分明功能专一6,6.2 函数的定义与调用,在,C,语言中,把逻辑上独立,能完成规定任务的模块称为函数或函数组。,一个源程序文件由一个或多个函数组成。一个源程序文件是一个编译单位,即以源程序为单位进行编译,而不是以函数为单位进行编译。,一个,C,程序由一个或多个源程序文件组成。一个源文件可以为多个,C,程序公用。,C,程序的执行从,main,函数开始,调用其他函数后流程回到,main,函数,在,main,函数中结束整个程序的运行。,main,函数是系统定义的。,所有函数都是平行的,即在定义函数时是互相独立的,一个函数并不从属于另一函数,对于编制好的函数,需要时可随时加以调用,但不能调用,main,函数,。,7,6.2 函数的定义与调用在C语言中,把逻辑上独立,能完成规定,一、函数分类,函数按,主次,分为:,主函数,和,非主函数,两种。主函数是名为,main,的函数,是一个源程序不可缺少部分;,函数按,编程技术,分为:,有返回值,的函数和,无返回值的,函数;,函数按,编制者,分为:,库函数(标准函数,)和,自定义函数,:前者由系统提供,只要在#,include,命令中写上相应的头文件,就允许用户直接调用;后者是用户自己定义的函数,解决用户的专门需要。,函数,按参数,分为:,无参函数,和,有参函数,两种,。,有参函数指在调用函数时,在主调函数和被调用函数之间有数据传递。也就是说,主调函数可以将数据传给被调用函数使用,被调用函数中的数据也可以带回来供主调函数使用。,8,一、函数分类函数按主次分为:主函数和非主函数两种。主函数是名,二、标准库函数与自定义函数,C,语言有丰富的库函数,这些函数的说明在不同的头文件(*.,h),中。,想要调用标准的库函数,就必须,include。,#include,main,(),printf,(,“%d”,1024*768,);,调用,printf,函数时,必须,include,9,二、标准库函数与自定义函数C语言有丰富的库函数,这些函数的说,自定义函数,可以把完成一个任务的过程写成函数。,int A_to_a(int capital)int small; if (capital=A ,返回值类型名,函数名,注意不要与已有库函数重名,参数说明和参数列表,调用函数时输入参数的格式要与之相同,定义局部变量,最好只使用局部变量,这样将方便调试。,返回值,如果不需返回则可,return 0;,另外请注意这样的判断,如写成,Acapitaly? x:y;,return(z);,3、可以有“空函数”,它的形式为:,类型说明符 函数名(), ,例如:,dumy() ,12,例如:3、可以有“空函数”它的形式为:例如:12,4、对形参的声明的传统方式,在老版本,C,语言中,对形参类型的声明是放在函数定义的第2行,也就是不在第,l,行的括号内指定形参的类型,而在括号外单独指定,例如:,int max(x,y),int x,y;, int z;,z=xy?x:y;,return(z);,一般把这种方法称为传统的对形参的声明方式,而把前面介绍过的方法称为现代的方式。,Turbo C,和目前使用的多数,C,版本对这两种方法都允许使用,两种用法等价。,13,4、对形参的声明的传统方式 在老版本C语言中,对形,1、形式参数和实际参数,在定义函数时函数名后面括弧中的变量名称为“,形,式参数,”,在主调函数中调用一个函数时,函数名后面,括弧中的参数(可以是一个表达式)称为“,实际参数,”。,三、函数参数和函数的值,14,1、形式参数和实际参数 在定义函数时函数名后面,int max(int a,int b) int y; y=(ab)? a:b; return y; ,同样的,在调用,m=max(a,b),时,其形参的值是,a,和,b,而,m,将会得到,y,的值,调用时:,m=max(3,6); m=max(a,b);,括号里是形式参数,返回值,括号里是实参,在这一句调用时,形参的值是3和6,其返回值,y,将被赋给调用语句中的,m,函数的参数,15,int max(int a,int b) int,调用函数时的数据传递,main(),int a,b,c;,scanf(“%d,%d”,c=max(a,b);,printf(“Max is %d”,c);,max(int x,int y),int z;,z=xy?x:y;,return(z);,运行情况:,7,8,Max is 8,例:,16,调用函数时的数据传递main()运行情况:例:16,关于形参与实参的说明:,(1)在定义函数中指定的形参,在未出现函数调用时,它们并,不占内存中的存储单元。只有在发生函数调用时,函数,max,中的形参才被分配内存单元。在调用结束后,形参所,占的内存单元也被释放。,(2)实参可以是常量、变量或表达式,如:,max(3,a+b);,但要求它们有确定的值。在调用时将实参的值赋给形参,(如果形参是数组名,则传递的是数组首地址而不是数组,的值)。,17,关于形参与实参的说明:(1)在定义函数中指定的形参,在未出现,(3)在被定义的函数中,必须指定形参的类型。,(4)实参与形参必须个数相同,实参与形参的类型应相同或赋值兼容。,(5),C,语言规定,实参变量对形参变量的数据传递是“值传递”,,即单向传递,只由实参传给形参,而不能由形参传回来给,实参,在调用函数时,给形参分配存储单元,并将实参对,应的值传递给形参,调用结束后,形参单元被释放,实参,单元仍保留并维持原值。,函数的返回值,(1)函数的返回值是通过函数中的,return,语句获得的。,return,语句将被调用函数中的一个确定值带回主调函数中去。,18,(3)在被定义的函数中,必须指定形参的类型。(4)实参与形参,return,语句后面的括弧也可以不要,如:,return z;,return,后面的值可以是一个表达式。,max(int x,int y),return(xy?x:y);,(2)函数值的类型。,在定义函数时指定函数值的类型。例如:,int max(float x,float y),函数值为整型,char letter(char c1,char c2),函数值为字符型,double min(int x,int y),函数值为双精度型,19,return 语句后面的括弧也可以不要,如:return后面,C,语言规定,凡不加类型说明的函数,一律自动按整型处,理。在定义函数时对函数值说明的类型一般应该和,return,语句,中的表达式类型一致。,例 返回值类型与函数类型不同。,main(),float a,b;,int c;,scanf(“%f,%f,”,c=max(a,b);,printf(“”Max is%dn”,c);, max(float x,float y);, float z;,/,z,为实型变量,/,z=xy?x:y;,return(z); ,运行情况如下:,1.5,2.5,Max is 2,20,C语言规定,凡不加类型说明的函数,一律自动按,(3)如果函数值的类型和,return,语句中表达式的值不一致,则,以函数类型为准。对数值型数据,可以自动进行类型转,换。即函数类型决定返回值的类型。,(4)如果被调用函数中没有,return,语句,并不带回一个确定的、,用户所希望得到的函数值,但实际上,函数并不是不带回,值,而只是不带回有用的值。带回的是一个不确定的值。,(5)为了明确表示“不带回值”,可以用“,void”,定义“无类型”,(或称“空类型”),21,(3)如果函数值的类型和return语句中表达式的值不一致,,四、函数调用,1,、函数调用的一般形式,函数名(实参表列);,如果是调用无参函数,则“实参表列”可以没有,但括弧不,能省略,如果实参表列包含多个实参,则各参数间用逗号隔开。,实参与形参的个数应相等,类型应一致。实参与形参按顺序对,应,一一传递数据。如果实参表列包括多个实参,对实参求值,的顺序并不是确定的,有的系统按自左至右顺序求实参的值,,有的系统则按自右至左顺序。,许多,C,版本(例如,Turbo,C,和,MS C,),是按自右而左的顺序,求值。,22,四、函数调用1、函数调用的一般形式 函数名(实参表列);,2,、函数调用的过程,(1)计算实在参数表中各表达式;,(2)将表达式的值(此值可以是一般意义量的值,也可以是指针的值(地址)依次赋给,同类型,的各形式参数;,(3)控制转移到函数体,执行函数体;,(4)当遇到,return,语句中包含表达式时,则将表达式的值送回调用函数;没执行到,return,语句或虽执行到,return,语句但不包含表达式时,均没有确定值送回调用函数.,对,void,型函数,return,语句不能带表达式,.,在考察函数调用时,要注意:是以表达式形式调用函数还是以语句形式调用函数; 参数传递,是传值还是传地址; 有无返回值与函数类型的关系。,23,2、函数调用的过程(1)计算实在参数表中各表达式;,3,、函数调用的方式,按函数在程序中出现的位置来分,可以有以下三种函数,调用方式:,(1)函数语句:把函数调用作为一个语句。如:,printstar();,(2)函数表达式,函数出现在一个表达式中,这种表达式称为函数表达式。,这时要求函数带回一个确定的值以参加表达式的运算。例如:,c=2*max(a,b);,(3)函数参数,函数调用作为一个函数的实参。例如:,m=max(a,max(b,c);,24,3、函数调用的方式 按函数在程序中出现的位置来分,4,、对被调用函数的声明和函数原型,在一个函数中调用另一函数(即被调用函数)需要具备的条件,(1)首先被调用的函数必须是已经存在的函数(是库函数或用,户自己定义的函数)。,(2)如果使用库函数,一般还应该在本文件开头用,include,命,令将调用有关库函数时所需用到的信息“包含”到本文件中,来。例如,前几章中已经用过的,include ,(3)如果使用用户自己定义的函数,而且该函数与调用它的函,数(即主调函数)在同一个文件中,一般还应该在主调函,数中对被调用的函数作,声明,,即对编译系统声明将要调用,此函数,并将有关信息通知编译系统。,25,4、对被调用函数的声明和函数原型 在一个函数中调用另一函数(,例 对被调用的函数作声明,main(),float add(float x,float y); /,对被调用函数的声明,/,float a,b,c;,scanf(“%f,%f”,c=add(a,b);,printf(“sum is%f”,c);,float add(float x,float y); /,函数首部,/,float z; /,函数体,/,z=x+y;,return(z);,运行情况如下:,3.6,6.5,sum is 10.000000,26,例 对被调用的函数作声明main()运行情况如下:26,在函数声明中也可以不写形参名,而只写形参的类型。如:,float add(float,float);,在,C,语言中,以上的函数声明称为,函数原型,(,function prototype)。,它的作用主要是利用它在程序的编译阶段对调用函数的合,法性进行全面检查。,函数原型的一般形式为,函数类型 函数名(参数类型1,参数类型2),函数类型 函数名(参数类型1,参数名1,参数类型2,,参数名2),第种形式是基本的形式。为了便于阅读程序,也允许在,函数原型中加上参数名,就成了第种形式。但编译系统不检,查参数名。因此参数名是什么都无所谓。,27,在函数声明中也可以不写形参名,而只写形参的类型。如:,说明:,如果在函数调用之前,没有对函数作声明,则编译系统会把,第一次遇到的该函数形式(函数定义或函数调用)作为函数的,声明,并将函数类型默认为,int,型。,如果函数类型为整型,可以在函数调用前不必作函数声明。,但是使用这种方法时,系统无法对参数的类型做检查。若调用,函数时参数使用不当,在编译时也不会报错。因此,为了程序,清晰和安全,建议都加以声明为好。,如果被调用函数的定义出现在主调函数之前,可以不必加以,声明。,28,说明: 如果在函数调用之前,没有对函数作声明,则编译系统会,如果已在所有函数定义之前,在函数的外部已做了函数声,明,则在各个主调函数中不必对所调用的函数再作声明。,例如:,char letter(char,char);,/,以下3行在所有函数之前,且在函数外部,/,float f(float,float);,int i(float,float);,main(), /,不必声明它所调用的函数,/,char letter(char c1,char c2),/,定义,letter,函数,/,float f(float x,float y),/,定义,f,函数,/,int i(float j,float k),/,定义,i,函数,/,29, 如果已在所有函数定义之前,在函数的外部已做了函数声cha,6.3,函数嵌套与递归调用,一、函数的嵌套调用,C,语言的函数定义都是互相平行、独立的,也就是说在定义函数时,一个函数内不能包含另一个函数。,C,语言不能嵌套定义函数,但可以嵌套调用函数,也就是说,在调用一个函数的过程中,又调用另一个函数。,main,函数,a,函数,b,函数,调用,a,函数,调用,b,函数,结束,30,6.3函数嵌套与递归调用一、函数的嵌套调用 C,例:,编一个程序计算下列函数值;,要求为函数,p(i)、s(n)、f(x,y),均编写一个用户函数,,x、y,由主函数输入。,f(x,y)=,s(x),s(y),其中,s(n)= p(i)= p(1)+ p(2)+ p(n), p(i)=i!,n,i=1,31,例:编一个程序计算下列函数值; 要求为函数p(i),float p(long i), long k,j;,for(k=j=1;j=i;j+) k=k*j;,return(float)(k);,float s(long n), float sum=0.0;,long k;,for(k=1;k1),的递归调用函数。,float p(long n), if(n1,1,n=0,1,35,如果有一条调用语句“k=p(4);”,用图示的方式描述其执行,我们把向下的递归调用过程称为“,递归过程,”,把向上的携带返回值计算返回表达式的过程称为“,回溯过程,”。其中递归过程的关键在于每次递归时,参数的值都要逐步趋向某个特定值(例子中这个特定值是,1,)。当递归到参数值等于特定值时,递归过程结束,此时一定要有返回值。,递归的规则和递归的出口,36,我们把向下的递归调用过程称为“递归过程”,把向上的携,例,1.,有5个人坐在一起,问第5个人年龄?他说比第4个人大2岁;问第4个人年龄?他说比第3个人大2岁;问第3个人年龄?他说比第2个人大2岁;问第2个人年龄?他说比第1个人大2岁;最后问第1个人年龄,他说是10岁,请问第5个人的年龄?,则:,递归规则:,age(n)=age(n-1)+2,递归出口:,age(1)=10,2、,例,Hanoi,塔问题,37,例1.有5个人坐在一起,问第5个人年龄?他说比第4个人大2岁,例3.用递归方法求,n,阶勒让德多项式的值.,分 析:,递归规则就是,P,n,(x),与,P,n-1,(x),及,P,n-2,(x),的关系,递归出口:,n=1,或,n=0,的情况.,T tn=0? F,返回1,T tn=1? F,返回,tx,递归 调 用,P(),N-S,图:,38,例3.用递归方法求n阶勒让德多项式的值.分 析:T,例3程序,main(),int x,n;,float p();,printf(nPlease Input n and x:);,scanf(%d,%d,printf(N=%d,X=%d.n,n,x);,printf(P%d(%d)=%10.2f,n,x,p(n,x);,float p(tn,tx),int tn,tx;, if (tn=0),return(1);,else if (tn=1),return(tx);,else,return(2*tn-1)*tx*p(tn-1),tx)-(tn-1)*p(tn-2),tx)/tn);,运行结果:,Please Input n and x:0,7,N=0, X=7.,P0(7)= 1.00,Please Input n and x:1,7,N=1, X=7.,P1(7)= 7.00,Please Input n and x:3,4,N=3, X=4.,P0(7)= 154.00,39,例3程序main()运行结果:39,例4.,递归法将一个整数,n,转换成字符串,例如输入483,应输出字符串“4 8 3”.,n,的位数不确定,可以是任意位数的整数.,分析:,1.,由于位数不确定,所以不可能先直接计算高位的字符,只能采用递归的形式,从个位开始“进入递归”,到最高位则可以达到递归出口,然后再“回溯”。,2.要考虑输入负整数的情况.,number,为负,T,F,输出负号,使,number,变为正数,递归调用,convert,函数,输出字符,输入整数,number,N-S,图:,40,例4.递归法将一个整数n转换成字符串,例如输入483,应输出,例4程序,#,include stdio.h,void convert(int n),int i;,if (i=n/10)!=0),convert(i);,putchar(n%10+0);,putchar( );,main(), int number;,printf(nPlease Input:);,scanf(%d,printf(nOutput is:);,if (numberb?a:b;,return(c);,main(), int a=8;,/,a,为局部变量,/,printf(“%d”,max(a,b);,形参,a、b,作用范围,a、b,作用范围,局部变量,a,作用范围,全局变量,b,的作用范围,运行结果为:,8,48,例 全局变量与局部变量同名。int a=3,b=5;,例题分析,#,include ,int a,b; /*a,b,为全局变量*/,void f1(int x ), int t1,t2,a;,a=t1 = x* 4;,t2 = b * 3;,b = 10;,printf (“f1:t1=%d,t2=%d,a=%d,b=%dn”, t1,t2,a,b);,main( ), a=2; b=4; /*,此,a,b,是全局变量,赋值,*/,f1( a); /*,调用函数,f1( ),*/,printf (“main: a=%d,b=%d”, a, b);,f1:t1=8,t2=12,a=8,b=10,main:a=2,b=10,输出结果:,49,例题分析#include f1:t1=8,若将程序改为:,#,include ,int a=2,b=4; /*a,b,为全局变量*/,void f1( ), int t1,t2;,t1 = a * 2;,t2 = b * 3;,b = 100;,printf (“f1:t1=%d,t2=%d,b=%dn”, t1, t2,b);,main(), int b=4; /*,此,b,是局部变量,赋值 */,f1( ); /*,调用函数,f1( ) */,printf (“main:a=%d,b=%d”, a, b);,f1:t1=4,t2=12, b=100,main:a=2,b=4,输出结果:,注意与课本,P80,的例题比较!,结论,:,全局变量与局部变量同名时,局部变量,起作用,全局变量被屏蔽(不影响),应小,心使用,50,若将程序改为:f1:t1=4,t2=12, b=100输出结,二.变量的存储特性,1.变量按存在时间分,静态变量,动态变量,静态存储,类型的变量的生存期为程序执行的整个过程,在该过程中占有固定的存储空间,通常称它们为,永久存储,。,动态存储,类型变量,只生存在某一段时间内,。例如,函数的形参和函数体或分程序中定义的变量, 只是在程序进入该函数或分程序时才分配存储空间, 当该函数或分程序执行完后,,,变量对应的存储空间又被撤销了,。,2.,c,语言中每一个变量有两个属性:数据类型,存储特性,完整的变量定义:,存储特性 数据类型 变量名;,51,二.变量的存储特性静态存储类型的变量的生存期为程序执行的整个,3.变量的存储特性,自动型,auto,静态型,static,寄存器型,register,外部型,extern,(1) auto,型,每次进入程序是自动分配内存,不长期占用内存例如:形式参数,自动型局部变量,(2),static,型,局部静态变量 全局静态变量,长期占用内存,52,3.变量的存储特性52,例1,:分析执行结果,f(int a),int b=0; static int c=3;,b+;c+;,printf(“%5d%5d%5d”,a,b,c);,return(a+b+c);,main(),int a=2,k;,for(k=0;k3;k+),printf(“%5dn”,f(a);,静态变量只初始化一次,结果:,2 1 4 (,a,b,c),7 (f(a),2 1 5,8,2 1 6,9,53,例1:分析执行结果静态变量只初始化一次结果:53,例2,打印1到5的阶乘值。,int fac(int n), static int f=1;,f=f*n;,return(f);,main(), int i;,for(i=1;i(y)? (x):,(y),main(), int a,b,c,d,t;,t= MAX(a+b,c+d);,#,define squ(n) n*n,void main ( ),printf (%fn,27.0/squ(3.0);,程序输出结果为: 27.000000,注意,展开为27.0/3.0*3.0,不是,27.0/(3.0*3.0),72, 使用宏次数多时,宏展开后源程序长,因为每展开一次都,赋值语句展开后为,t(ab)(cd)? (ab):(cd);,注意:,MAX,不是函数,这里只有一个宏,在,main,函数中就能,求出,t,的值。这个问题也可用函数来求:,int max(int x,int y), return(xy?x:y);,main(), int a,b,c,d,t;,t= max(a+b,c+d);,max,是函数,在,main,函数中调用,max,函数才能求出,t,的值。,73,赋值语句展开后为int max(int x,int y)ma,例:比较下列两个程序:理解宏替换与函数调用的不同。,程序1:,main(),int a,b;,scanf(,%d%d,printf(,a=%d,b=%dn,a,b);,swap(a,b);,printf(,a=%d,b=%dn,a,b);,swap(int x,int y),int t;,t=x;x=y;y=t;,程序2:,#,define SWAP(x,y) t=x;x=y;y=t,main(),int a,b,t;,scanf(,%d%d,printf(,a=%d,b=%dn,a,b);,SWAP(a,b); /*,注意大写*/,printf(,a=%d,b=%dn,a,b);,74,例:比较下列两个程序:理解宏替换与函数调用的不同。程序1:程,#,define PR printf,#define NL n”,#define D ”%d,#define E %d,#define D1 D NL,#define D2 D E NL,#define D3 D E E NL,#define D4 D E E E NL,#define S “%s”,main(),int a,b,c,d;,char string=“CHINA”;,a=1,b=2,c=3,d=4;,PR(D1,a);,PR(D2,a,b);,PR(D3,a,b,c);,PR(D4,a,b,c,d);,PR(S,string);,运行时输出以下结果:,1,12,123,1234,CHINA,例:,75,#define PR printf运行时输出以下结果:例:,二、文件包含,文件包含处理是指一个源文件将另一个指定文件的全部内容嵌入在文件包含命令处,成为自己的一部分。,76,二、文件包含文件包含处理是指一个源文件将另一个指定文件的全部,1,、一般形式:,include “,文件名”,或,include ,2,、例:可以将上面的例程改为:,(1)文件,format.h,#define PR printf,#define NL n”,#define D “%d,#define E %d,#define D1 D NL,#define D2 D E NL,#define D3 D E E NL,#define D4 D E E E NL,#define S “%s”,(2)文件,file1.c,#include “format.h”,main(),int a,b,c,d;,char string=“CHINA”;,a=1,b=2,c=3,d=4;,PR(D1,a);,PR(D2,a,b);,PR(D3,a,b,c);,PR(D4,a,b,c,d);,PR(S,string);,77,1、一般形式:include “文件名” 或 incl,3、,说明,(1)一个,include,命令只能指定一个被包含文件,如果要包含,n,个文件,要用,n,个,include,命令。,(2)如果文件1包含文件2,而文件2中要用到文件3的内容,则,可在文件1中用两个,include,命令分别包含文件 2和文件3,,而且文件3应出现在文件2之前,即在,filel.c,中定义:,