570 likes | 709 Views
第四章 函数. 模块化程序设计是进行大程序设计的一种有效措施。其基本思想是将一个大的程序按功能进行分割成一些模块,使每一个模块都成为功能单一、结构清晰、接口简单、容易理解的小程序。. C 语言所支持的模块化软件开发的功能. 函数式 的程序结构。程序整体由一个或多个函数组成。每个函数都具有各自独立的功能和明显的界面。 允许通过使用不同 存储类别 的变量,控制模块内部及外部的信息交换。 具有 编译预处理 功能,为程序的调试、移植提供了方便,也支持了模块化程序设计。. 函 数. C/++ 程序的结构 函数的定义、声明和调用( 嵌套调用、递归调用 )
E N D
第四章 函数 模块化程序设计是进行大程序设计的一种有效措施。其基本思想是将一个大的程序按功能进行分割成一些模块,使每一个模块都成为功能单一、结构清晰、接口简单、容易理解的小程序。
C语言所支持的模块化软件开发的功能 • 函数式的程序结构。程序整体由一个或多个函数组成。每个函数都具有各自独立的功能和明显的界面。 • 允许通过使用不同存储类别的变量,控制模块内部及外部的信息交换。 • 具有编译预处理功能,为程序的调试、移植提供了方便,也支持了模块化程序设计。
函 数 • C/++程序的结构 • 函数的定义、声明和调用(嵌套调用、递归调用) • 函数间的参数传递(传值调用、传地址调用、函数返回值) • 传值参数(行参为普通变量、数组元素、对象) • 指针参数(行参为指针、行参为常指针、引用参数、数组名) • 函数参数缺省、函数重载、函数模板 • 作用域与存储类别 • 变量的存储类型(自动变量、静态局部变量和全局变量) • 内部函数和外部函数
C程序结构 main() { ... f1( ); … f2( ); } f1( ) { … f11( ); … } f11( ) { … } f21( ) { … } f2( ) { … f21( ); f22( ); } f22( ) { … }
C/C++程序的结构特点 • 无论涉及的问题是复杂还是简单,规模是大还是小,用C语言设计程序,任务只有一种,就是编写函数,至少也要编写一个main()函数。 • 一个C语言源程序由一个或多个函数组成,一个源程序文件是一个编译单位,即编译是以源程序为单位,而不是以函数为单位。 • 一个C程序由一个或多个源程序文件组成,对于较大的程序一般不会将所有程序全放在一个文件中;一个源文件也可以为多个程序公用。
C程序的结构特点 • 一个C程序由一个或多个函数组成,但主函数main( )只能一个。C程序从main( )函数开始执行,调用其它函数后流程回到main( )函数,在main( )函数执行完后结束整个程序的运行。 • 所有函数(不包括成员函数)都是平行的,定义的每一个函数是独立的。函数间可以互相调用,但不能调用main( )函数。 • 函数的分类: • 标准库函数 、 用户自定义函数 • 有参数函数、无参数函数 • 有返回值函数、无返回值函数
4.1 函数的定义、声明和调用 #include “iostream.h” void main ( ) { int a,b,c,m; int max(int x, int y); /*函数的声明*/ cout<<“输入三个整数:“ ; cin>>a>>b>>c; m=max(a,b); /*函数的调用*/ m=max(m,c); cout<<“最大的数是:”<<m<<endl; } int max(int x, int y) /*函数的定义 */ { int z; z=x>y?x:y; return(z); }
函数的定义 • 函数定义就是在程序中设定一个函数模块,它是由变量声明部分与可执行语句组成的独立实体,用以完一指定的功能。 • 函数定义的内容包括: • 函数类型(即函数值类型) • 函数名 • 形式参数的数目、类型 • 函数体(操作及返回值) • 函数名应符合C语言对标识符的规定
函数定义的一般形式: 类型标示符 函数名(类型标示符 形参,…) { 变量定义部分 执行语句部分 } 函数体 float max(float x,float y) { float z; z = x>y? x :y ; return z; }
形式参数: • 形式参数写在函数名后面的一对圆括号内,每个形式参数由数据类型标示符和参数名组成,形参之间应以逗号相隔。 • 形式参数有两个作用: • 表示将从主调函数中接收哪些类型的信息。 • 在函数体中形式参数是可以被引用的,可以输入、输出、被赋以新值或参与运算。 • 注意:有些函数没有形式参数。这时,主函数main后面的圆括号内可以为空,其它函数应该在圆括号内写上void以声明它为空。
函数体: • 函数体是一个分程序结构,由变量定义部分和语句组成。在函数体中定义的变量只有在执行该函数时才存在。 • 函数体中也可以不定义变量而只有语句,也可以二者皆无。如: void null(void){ } 这是一个空函数,调用它不产生任何有效操作,但却是一个C语言的合法函数。 • 在模块化设计中,往往先把main函数写好,并预先确定需要调用的函数,有时一些函数还未编写好,可以用这样的空函数(函数名用将来使用的实际函数名)放在程序中,以便调试程序的其它部分,以后再逐步补上。
函数的返回: • 返回是函数执行的最后一个操作。返回的意义是: • 使流程返回主调函数,宣告函数的一次执行终结,在调用期间所分配的变量单元被撤销。 • 送函数值到调用表达式中。但是这一点并非是必需的。有些函数有返回值,有些函数可以没有返回值。 • 函数的返回值是通过return 语句获得。return语句可以不带表达式,表示不返回任何值,应将其函数类型定义为void,它位于函数体最后时可省略。 • 在一个函数中允许有多个return语句,流程执行到任何一个return语句都会返回到主调函数。如果多个return语句返回值类型不一致,编译时会给出警告。 float y (float x) { if (x> 0)return(x*x-x+1); else return(x*x*x+x+3); }
函数的类型: • 通常把函数返回值的类型称为函数的类型,即函数定义时所指出的类型。 • 函数在返回前要先将表达式的值转换为所定义的类型,再返回到主调函数中的调用表达式。即如果函数值的类型和return 语句中表达式的值得类型不一致,则以函数定义为准,函数类型决定返回值的类型。 • int型与char型函数在定义时可以不定义类型(即不写int或char),系统隐含指定为int型。 • 对不需要使用函数返回值的函数,应定义为void类型。
函数定义的外部性: • 函数不能嵌套定义:一个函数不能定义在别的函数的内部。或者说,函数只能定义在别的函数的外部,它们都是平行的,互相独立的,即不允许出现如下形式的定义: • 一个程序如果用到多个函数,允许把它们定义在不同的文件中,也允许一个文件中含有不同程序中的函数。即在一个文件中可以包含本程序用不到的函数,它们不被本程序调用。 /*不允许出现如下形式的定义*/ Func1() { … Func2() { … } … } /*应改为如下形式的定义*/ Func1() { … } Func2() { … }
函数声明 • 函数声明是对所用到的函数的特征进行必要的声明。编译系统以函数声明中给出的信息为依据,对调用表达式进行检测,以保证调用表达式与函数之间的参数正确传递。在主调函数中,要对在本函数中将要调用的函数事先作一声明。 • 函数声明的一般形式: 类型标识符 函数名(类型标识符 形参,…); 例如:float max(float a, float b); • 这些信息就是函数定义中的第一行(称函数头)的内容,也称函数模型(或函数原型)。 • 句末的分号不可少
函数声明 • 形参的名字是不重要的,重要的是类型标识符。上面的函数声明也可以不写形参名: float max(float, float); • 不能只写形参名而不写类型: float max(a, b); • 不能不写函数的类型 max(float, float); 只有函数返回值为int或char时,函数类型标识符才可以省略
函数声明 • 下列情况可不对被调函数声明: • 被调函数的定义出现在主调函数之前。 • 已在所有函数定义之前,在文件的开头,在函数的外部已说明了函数类型 • 是为了便于阅读和理解,应当养成在调用之前都做显式声明的风格。 • 当一个函数要被一个文件中的多个函数调用时,可以将该函数声明写在所有函数之前。
函数的调用 • 函数调用的一般形式: 函数名(实参列表); • 实参与形参的个数相等、类型一致,按顺序一一对应。 • 调用无参函数,实参列表可以没有,但括弧不能省略。 • 函数调用的方式: • 函 数 语 句: printf(“Hello\n”); • 函数表达式: c=2*max(a,b); • 函数的参数: m=max(a,max(b,c));
函数的嵌套调用 • C函数不允许将函数定义在另一个函数体中,但允许在调用一个函数的过程中又调用另一个函数。这种调用称为函数的嵌套调用。
函数的递归调用 • 递归就是某一事物直接或间接地由自己组成。如画家画自己正在画自己的情景。 • 一个函数直接或间接地调用自己,便构成了函数的递归调用,前者称直接递归调用,后者称为间接递归调用。
4.2 函数间的参数传递 • 参数是函数调用时进行信息交换的载体。 • 实参是调用函数中的变量,形参是被调函数中的变量。在函数调用过程中实现实参与形参的结合。 • 在C/C++程序中,实参与形参是按传值方式相结合的,也称为传值调用(call by value)方式。
函数的传值调用 • 传值调用的过程是: • 形参与实参各占一个独立的存储空间。 • 形参的存储空间是函数被调用时才分配的。调用开始,系统为形参开辟一·个临时存储区,然后将各实参之值传递给形参,这时形参就得到了实参的值。这种虚实结合方式称为“值结合”。 • 函数返回时,临时存储区也被撤销。 • 传值调用的特点:函数中对形参变量的操作不会影响到调用函数中的实参变量,即形参值不能传回给实参。
注意事项 • 实参可以是常量、变量、表达式,但要有确定的值;在调用时将实参的值赋给形参变量--“值传递”。 • 实参与形参的数据个数、类型应一致. • 形参的值不能传回给实参。 • 形参可以有多个,函数返回值只能一个。
例: 交换变量的值 main( ) { int a=3, b=5; void swap (int x, int y); swap (a,b); cout<<"a=“<<a<<“, b=“<<b<<endl; } void swap (int x, int y) { int temp; temp=x, x=y, y=temp; cout<<“x=“<<x<<“, y=“<<y<<endl; } • 运行结果为 x=5, y=3 a=3, b=5
*x 1000 5 *y 1002 3 函数的传地址调用 void swap( int *x, int *y) { int t; t=*x; *x=*y; *y=t; } main( ) /*传地址调用*/ { int a=3, b=5; void swap(int *x,int *y); swap(&a, &b); cout<<a<<“,”<<b<<endl ; } • 主函数中a和b的值被改变。
x 1000 5 y 1002 3 传地址调用(引用参数) void swap( int &x, int &y) { int t; t=x; x=y; y=t; } main( ) /*传地址调用*/ { int a=3, b=5; void swap(int &x,int &y); swap(a, b); cout<<a<<“,”<<b<<endl ; } • 主函数中a和b的值被改变。
传地址调用(常引用参数) main( ) /*传地址调用*/ { int a=3, b=5; void swap(const int &x, const int &y); swap(a, b); cout<<a<<“,”<<b<<endl ; } void swap( const int &x, const int &y) { int t; t=x; x=y; y=t; } • 不允许修改const对象
传地址调用(常引用参数) main( ) /*传地址调用*/ { int a=3, b=5; void swap(const int *x, const int *y); swap(&a, &b); cout<<a<<“,”<<b<<endl ; } void swap( const int *x, const int *y) { int t; t=*x; *x=*y; *y=t; } • 不允许修改const对象
4.3 函数的递归调用 • 递归就是某一事物直接或间接地由自己组成。如画家画自己正在画自己的情景。 • 一个函数直接或间接地调用自己,便构成了函数的递归调用,前者称直接递归调用,后者称为间接递归调用。
例:用函数递归求 n! • 算法分析:n!=n*(n-1)!,1!=1 long fact(unsigned n) { if (n<=1) return(1); else return(n*fact(n-1)); }
main F(3) F(2) F(1) 3*F(2) 2*F(1) F(1) 4*F(3) 返回1 结果24 返回6 返回2 4*3! 3*2! 2*1! 1!=1 n!=n*F(n-1)的递归过程
函数的递归调用 • 递归函数在执行时,将引起一系列的调用和回代的过程 • 递归过程不应无限制地进行下去,当递归若干次后就应该到达终点得到一个确定的值,然后进行回代。回代的过程是从一个已知的值推出下一个值,实际上时一个递推过程 • 任何有意义的递归总是由两部分组成:递归方式与递归终止条件 • 当一个问题蕴含递归关系,且结构比较复杂时,采用递归算法往往比较自然、简洁、容易理解。
例 Hanoi(汉诺)塔问题 • 有三根秆A,B,C。A秆上有64个盘子,大的在下小的在上。要把这64个盘子从A移到B,在移动过程中可以借助C,每次只允许移动一个盘子,且在移动过程中在三根秆上都保持大盘在下,小盘在上。要求编出程序打印出移动的步骤。
c a b ① 1 ③ 2 1 3 2 ② 3 4 4 汉诺塔问题 • 将A上的n-1个盘借助B先移到C上: hanoi(n-1,a,c,b) • 将n-1个盘从C上借助A移到B上。 hanoi(n-1,c,b,a) • 将A上第n个盘移到B上
汉诺塔问题 void hanoi( int n, char a,char b,char c) { if(n>0) { hanoi(n-1,a,c,b); printf("%d: %c-->%c\n",n,a,b); hanoi (n-1,c,b,a); } } main( ) { int n; printf("input n:"); scanf("%d",&n); hanoi(n,'A','B','C'); }
程序示例 #include “iostream.h” #include “math.h” double s(double x,double eps=1e-6) { int n=1; double w=0,t=1; while (fabs(t)>=eps) { w += t; t=t*x/(n++); } return w; } void main( ) { cout<<“s1=“<<s(2.0)<<endl; cout<<“s2=“<<s(3.0,1e-5)<<endl; }
4.5 作用域与存储类别 • 数据类型是变量的操作属性。除此之外,影响变量使用的还有变量的存储属性。 • 变量的存储器类型:内存变量和寄存器变量 • 变量的生存期(存在性): • 永久存储:在编译时分配存储单元,程序执行结束即被撤销。 • 动态存储:只在程序执行的某一段时间内存在。 • 变量的可用域(可见性):全局可用与局部可用
存储属性 • 在C语言中,用“存储属性”来表示以上三个方面的属性。并且把它们分为四类:register,auto,static,extern。这四类存储属性的性质如下表所示。
动态变量 • 动态变量是在程序执行的某一时刻被动态地建立并在另一时刻被动态地撤销的一种变量,它们存在于程序的局部,也只在这个局部中可以使用。 • 动态变量有两种: • 自动(Auto)变量 • 寄存器(register)变量
动态变量 • 自动变量 特点: • 自动变量是局部变量:只在定义它的那个局部才能使用。 • 未赋值之前,它的值是不确定的。 • 对同一函数的两次调用之间,自动变量的值是不保留的,因为每次调用后其所用存储单元都被释放 [auto] 数据类型 变量名 [=初值表达式],…;
动态变量 • 如果缺省存储类型标示,系统隐含此变量是auto。 • 函数的形参也是一种自动变量,但说明时不加auto。 • 使用自动变量有以下好处: • “用之则建,用完即撤”,可以节省存储空间。 • 程序员可以独立地给本区域命名变量,无须关心程序的其它局域使用了什么变量。即使使用了与其它区域同名的变量,系统也把它们看作不同的变量。 • 在一个局部中定义需要用的变量,便于阅读和理解程序。
动态变量 • 寄存器变量 • 寄存器变量具有与自动变量相同的性质,但速度快 • 通常把使用频率较高的变量(循环变量)定义为register类别。函数的形参也可使用寄存器变量 • 由于各种机器寄存器数目不等且有限,当把一个变量指定为寄存器存储类别时,系统将努力实现它,不能实现时将把它视为auto型变量。 • 局部静态变量、全局变量不能定义为寄存器变量。 register 数据类型 变量名 [=初值表达式],…;
使用寄存器变量实例 • 寄存器变量不能进行取地址运算,如&i。所以也不能用scanf函数输入; int fac(int n); { register int i, f= 1; for ( i=1; i<=n; i++) f = f * i; return(f); } main( ) { int i; for ( i=1; i<= 5;i++) printf(“%d!=%d\n”, i, fac(i)); }
静态变量 • 静态变量在编译时分配存储空间,在程序的整个运行期间地址是固定的 • 静态变量的初始化是在编译时进行的,未显式初始化编译系统将其初始化为0 • 在函数被多次调用的过程中,静态局部变量的值具有可继承性 • 局部静态变量只能在本函数或分程序中使用。虽然函数调用结束后其值虽然保留,但其它函数不能引用它。 static数据类型 变量名 [=初值表达式],…;
局部静态变量实例 main( ) { int i; for(i=0;i<3;i++) f( ); } void f( ) { int a = 0; a++; printf("%d\t",a); } /*结果: 1 1 1 */ a b main( ) { int i; for(i=0;i<3;i++) f( ); } void f( ) { static int a = 0; a++; printf("%d\t",a); } /*结果: 1 2 3 */ 静态变量 自动变量
外部变量 • 外部变量是全局变量 • 它是在函数的外部定义的,编译时放在静态存储区。可被程序中的各个函数引用。 • 外部变量可以出现在函数之间,它的作用域从其定义点开始到文件末位。 • 一个C程序可以由一个或多个源程序文件组成,一个文件中的函数可以引用另一文件中的全局变量。
外部变量 • 限定外部变量只在本文件中使用 在外部变量前加static,使其有线局部化,称其为静态外部变量 文件:f1.c static int a; main( ) { ….. } f1( ) { …… } 本文件中的全局变量 其它文件中的函数不 可使用