1、第五章 函数机制 Chapter 5 Function Mechanism,2,函数的简介,创建程序的语句块 在不同的程序设计语言中的名称: Procedures, subprograms, methods In C+: functions I-P-O Input Process Output 任何程序中的基本构件 使用函数来完成,函数C+的函数是完成既定任务的功能(过程)体,它涵盖了数学函数和一般过程所以基于过程编程本质上就是基于函数编程 函数机制一是指程序运行过程中对函数调用的数据管理和处理过程二是指编程中函数的使用规范它包括函数参数的属性和传递规则,函数返回类型的匹配与审查,函数名字的识
2、别原则,函数体效率的选择,函数体中数据的访问权限等,4,预定义的函数,C+提供了预定义的函数库! 两种类型: 返回(产生)值的函数 无返回值的函数 (void函数) 许多“#include” 包含合适的函数库 e.g., , (最初的 “C” 语言库)(for cout, cin),5,程序员定义的函数,写自己的函数! 程序的子部分 单独 & 复合 可读性 可重用 你所定义的函数: 放在 main()的主函数文件中 也可以单独放在一个其他的文件中来调用,6,使用函数的组成部分,函数使用的3个部分: 函数声明/函数原型 提供给编译器必要的信息 使用一个函数调用所需要知道的内容 函数定义 实际执行
3、的函数代码 函数调用 函数的控制转移,7,函数声明,也称为函数原型 一个给编译器的 “信息” 声明 告诉编译器调用的含义 语法: FnName(); 例如: double totalCost( int numberParameter, double priceParameter); 在调用前放置的位置 放在 main()中的声明空间 或者放在main()前,在全局空间中,8,函数的声明,函数是面向对象程序设计中,对功能的抽象 函数声明的语法形式类型标识符 函数名(形式参数表) 语句序列 ,函数的声明与使用,是被初始化的内部变量,寿命和可见性仅限于函数内部,若无返回值,写void,9,函数的声明
4、,形式参数表name1, name2, ., namen 函数的返回值 由 return 语句给出,例如: return 0 无返回值的函数(void类型),不必写return语句。,函数的声明与使用,10,函数定义,函数执行 就像main()主函数的执行 例如: double totalCost( int numberParameter, double priceParameter) const double TAXRATE = 0.05; double subTotal; subtotal = priceParameter * numberParameter; return (subtot
5、al + subtotal * TAXRATE); 注意合适的缩进,11,函数定义放置的位子,放在main()主函数后 不要 “插在” main()函数中! 函数都是 “平等的”;没有函数是另一个函数的一“部分”函数的定义如同一个小程序,调用一个函数相当于运行这个小程序。 定义中的形式参数 数据传送的“占位符” ,利用形参来输入。 函数的实参就是实际输入的值,它们会在形参的位置插入。 返回语句 返回给调用者数据,函数会返回一个值,就好像函数的“输出”,14,函数调用,就和调用预定义函数一样 bill = totalCost(number, price); 被调用: totalCost 返回do
6、uble类型 赋值给变量名 “bill“ 这里的实参: number, price 被调实参可以是文字常量, 变量,表达式或者它们的组合 被称为实参是因为它们是真正被传送的数据,15,函数的调用,调用前先声明函数原型: 在调用函数中,或程序文件中所有函数之外,按如下形式说明:类型标识符 被调用函数名(含类型说明的形参表); 调用形式 函数名(实参列表) 嵌套调用 函数可以嵌套调用,但不允许嵌套定义。 递归调用 函数直接或间接调用自身。,函数的声明与使用,16,调用函数的函数,经常使用! main() 就是一个函数! 必要条件: 函数声明必须在使用函数之前完成 函数的定义放置的在 在 main(
7、)函数定义之后 或者在其他文件中,使函数能由几个不同的程序使用。 函数也可能调用自身 “递归“函数声明可以放在*.h文件中,18,例3-5,函数的声明与使用,#include #include using namespace std; double tsin(double x); int main() double k,r,s; coutr;couts;if (r*r=s*s)k=sqrt(tsin(r)*tsin(r)+tsin(s)*tsin(s);elsek=tsin(r*s)/2;coutkendl; ,19,double tsin(double x) double p=0.00000
8、1,g=0,t=x;int n=1;do g=g+t;n+;t=-t*x*x/(2*n-1)/(2*n-2);while(fabs(t)=p); return g; ,运行结果: r=5 s=8 1.37781,20,21,函数调用的执行过程,函数的声明与使用,22,嵌套调用,函数的声明与使用,main调fun1()结束,fun1()调fun2()返回,fun2()返回,第五章内容,函数性质( Function Character ) 指针参数 ( Pointer Parameters ) 栈机制 ( The Stack Mechanism ) 函数指针 ( Function Pointers
9、 )命令行与main参数 ( Command line & The mains Arguments ) 递归函数 ( Recursive Functions ) 函数重载 ( Function Overload ),5.1.1 函数的形态,有些函数具有求值表现,即有返回值,它可以无输入参数,也可以带多个或一个参数。如:,等价的C+函数描述为:,long double f(long double x)if(x 0) return x*x;return 2; ,数学函数定义域为 ,而long double为 1.2104932,所以参数 表示范围为大于0时的平方根:,-1.2104932 x 1.
10、1102466,也有一些函数没有求值表现,无返回值。如:void print()cout“unsigned int f(unsigned int n)n” “if(n=1|n=2) return 1;n” “return f(n-1)+f(n-2);n”“n”; 也有一些表现为过程的函数,具有参数,如延迟n秒的函数: void delay(int n)for(int i=0; i n;i+)for(int j=0;j 100000000;j+); /该循环约耗时1秒 ,跨越数学函数的C+函数,有四种形态,返回类型 func ( 参数列表 ) ; 返回类型 func ( ) ; void fun
11、c ( 参数列表 ) ; void func ( ) ;,有返回类型的函数可以参加表达式运算,或者直接赋值给对应类型的变量,构成表达式语句。如:double s = sin(b)+1; /s定义语句中的函数调用s = cos(c)/2; /s赋值语句,无返回值类型的函数,不能以值的形式赋给其它变量或者参加运算,其调用只能独立构成一条语句,如:print(); /okint t =print(); /错,需要时,有返回类型的函数也可以像无返回类型的函数一样单独构成语句,如:char s130;char s2 = “hello”;strcpy(s1,s2); /单独构成语句,完成复制工作cout
12、strcpy(s1,s2); /复制,并且返回值供输出,函数是独立的,反映了一个过程的功能性,只对输入(参数)、输出(返回值)负责,计算在封闭的黑盒中进行.函数调用完毕后,一切现场恢复如旧,因此也意味着函数调用操作的可重复性,其结果不随调用的时间、地点而转移.如图5-1所示:,5.1.2 函数黑盒,图5-1 函数黑盒,函数黑盒性使得函数开发各负其责,无需关心别人开发的函数细节,只关注要拿来使用的函数的外在功能。如函数描述包括下列内容:函数名及函数访问修饰输入参数列表输出参数列表以及返回类型函数功能描述,黑盒原则:函数使用者应关注性能,而少去左右实现细节,int cost ( int n, in
13、t m ) return n*m; / 运输n次m斤 int cost ( int n, int m ) return m*n; / 运输m次n斤 / 选择一个最好的 int cost ( int n, int m ) return (nm? m:n)*10; / 保证运输次数最少 ,函数通过参数来传递输入数据,参数通过传值机制来实现。所谓传值即在函数被调用之时,用克隆实参的办法来创建形参。如:,5.1.3 传值参数,参数传递:形参是对实参的克隆,克隆必须遵守类型匹配规则,void f(Type a); /a为形参 void g()Type x;f(x); /x为实参 ,a实体,x实体,复制,
14、Type类型,Type类型,当有大数据量的函数进行参数传值的时候,如传递给函数一个数据整体的复制品时,如:vector mysort(vectorv);void f()int a =3,5,7,1,8,4,9,2,6,0;vector va(a,a+10);vector vb=mysort(va); / vector v=va; 大数据量的函数的流程图如图5-2所示:,图5-2 大数据流量的函数,上述大数据量的函数有效的方法是传递给mysort排序函数以一个数组指针或者容器引用,这样,传递的是指针和引用值,然后通过指针和引用的间访来实现数据的操作。从而使得函数调用才这么灵活,高效,无须去背大块
15、数据传递的沉重负担,如传递数组的方法:,5.2.1 指针和引用参数,void mySort(int* b, int size); void f() int a = 3, 5, 7, 1, 8, 4, 9;mySort(a, sizeof(a)/sizeof(a0); ,元素个数,传指针,数组是不能整体复制的,即:int a10;int b10 =a;/错b=a; /错,数组只能通过传递数组起始地址,达到使用该数组的目的。,传递指针:指针参数也是值传递的,指针值的真正用途是进行数据间访,以达到操作数据块(大小由之)的目的传递引用:引用参数本质上也是值传递的,它表现为名字传递,即以形参的名字来代替
16、实参名字如果实参不是实体名而是表达式,那么其表达式所对应的临时实体取名为形参,并要求其为常量引用意义:指针和引用参数的存在,使函数实际上可以访问非局部的数据区,函数的黑盒性便名存实亡但这并非一定坏事,指针是一把双刃剑,或灵巧或邪恶引用是为了防范指针非安全的无意操作,然而,传递指针和引用的特性还是被广泛应用,由于参数值可以通过传递指针和引用获得,所以它意味着函数可以访问不属于自己管辖的数据,如图5-3所示:,图5-3 数组传递,只传递数组的地址,减轻了参数传递过程中的数据复制量,但是排序是在数组a上操作的,数据的改变不是在mySort函数数据区内,等到函数返回后,数组a的内容发生了改变,而参数b
17、本身的值却没有任何改变! 这意味着函数运行的结果并不一定要用返回类型规定的返回值来反映,函数参数也能起到返回结果的作用。 函数可以传递多个输入参数,也可以让多个数据处理的对象发生变化。,5.2.2 函数的副作用,函数参数传递指针和引用是一把双刃剑,它的副作用是破坏本地的数据,破坏模块性。 函数的黑盒性是指不与外界发生以外沟通,只依赖输入参数,只送出返回结果,因此,它是结果可以重复的计算过程。但是指针和引用的参数传递展示了函数可以访问非本地数据的途径,从而破坏了函数的黑盒性!如:,void print(vector ,结果:7 6 2 3 9 4 35 3 1 1 6 2 2 7 6 2 3 9
18、 4 3,程序本意是将两个初始化了的数组赋给两个向量a和b,然后调用add相加,并输出这两个向量以及结果向量。但是结果却发现,main函数调用的add函数结果虽然对了,但是原始数据的向量a却被破坏了。这就是函数运行所带来的副作用!,问题就出在规定参数传递的声明上,传递引用,给了函数超限的权力,函数循着传递的引用名而既读又写地访问了引用的空间,而那一片引用空间不是函数所拥有的。因此,对应的手段就是在指针和引用参数上可以加const修饰,以此限制函数体中对参数的写操作。如,限制无意操作带来的意外副作用,vector add( / 向量加法const vector ,参数的const声明,框定了传递
19、的参数只能以形参规定的原则来操作。当模块设计或结构设计与编程设计相分离时,所规定的函数参数传递的常量性就能规范编程行为,不致产生副作用。,5.3.1 运行时内存布局,一个程序要运行,先将可执行程序文件装载到计算机的内存中。操作系统将程序装入内存后,将形成一个随时可以运行的进程空间,该空间分为四个区域,如图5-4所示:,图5-4 运行中的内存布局,代码区存放程序的执行代码 全局数据区存放全局数据、变量、文字量、静态全局量和静态局部量。 堆区存放动态内存,供程序随机申请使用。 栈区存放函数数据区,它动态地反映了程序运行中的函数状态,其运动轨迹正好用来观察函数的调用与返回。,5.3.2 栈区,栈是一
20、种数据结构,遵循后进先出的原则。 C+的函数调用整个过程就是栈空间操作的过程。函数调用时,C+做下列工作: 建立被调函数的栈空间,大小由函数定义体中数据量多少而定 保护被调用函数的运行状态和返回地址 传递参数 将控制权转交给被调函数 函数运行完成后,复制返回值到函数数据块底部 恢复被调用函数的运行状态 返回调用函数,调用一个函数,可以看作是一个栈中元素的压栈和退栈操作。 最初,操作系统将main函数压入栈中,标志着程序运行的开始,等到最后main函数退栈,程序也就运行结束了。,下面的程序在main函数中调用一个函数,该函数又调用另一个函数,它得到如图5-5所示的栈结构布局。栈区运动演示程序如下
21、:int funcA(int x,int y);void funcB(int ,funcB函数,funcA函数,main函数,图5-5 运行中的函数栈结构,main函数被OS(操作系统)调用时,在栈中安排了返回值位置,OS状态保护例程指针是指完成恢复工作的函数地址,函数为硬件设置,或为OS专用的函数。在main函数返回时,取其地址调用,然后取OS调用main时的地址,返回调用点。main函数也有参数。,main函数在进行变量a的赋值运算时调用funcA函数获得函数值。调用时,funcA函数安排了返回值空间,设置了恢复main函数状态的函数指针和返回main调用点地址。main函数状态就是调用了
22、funcA的瞬间,CPU的各个寄存器状态的总和。main随后进行便进行参数传递,即分别将实参a、b的值传给func的形参x、y。funcA又建立起自己的局部变量n,然后调用了funcB。,funcB是一个没有返回类型的函数,因此,创建funcB栈元素时没有给func预留返回值空间。funcB设置了返回funcA状态的恢复指针函数和返回地址后,进行了实参到形参的拷贝,该拷贝实际上建立了形参s引用实参n的对应关系,以至于修改s为8时,对应的n值也跟着为8,当函数funcB返回时,其栈区结构见图5-6.,funcA函数,main函数,图5-6 funcB返回时的栈结构,funcB返回时,数据仍留在栈
23、中,而函数结构已经消失,不再有funcB函数,有的只是funcA现场,带到funcA返回时,其栈区结构见图5-7.,main函数,图5-7 funcA返回时的栈结构,funcA的返回值时在return语句中计算出来而存放的位置,它在main函数中参加运算,实际上作为main函数的临时空间使用。系统将返回值8(函数funcA)赋值给局部变量a。当main函数返回时,OS将这块区域收回,以便下次有新的程序运行请求时,再次分配。,5.3.3 局部数据的不确定性,无法猜测的时,OS启动main函数时,栈中是否已经清空。main 函数返回的时候并没有清楚数据,因此假定局部变量只要未经初始化,其值总是不确
24、定的。下列程序证实了这一点:,未初始化局部数据的不确定性,#include void f()int b; / 未初始化std:cout”a“n”;f(); /- / 8804248 / 2788048,原因: 任何软件设计都有它的合理性。分配给申请运行的程序之内存空间时属于用户的,用户的内存怎么使用,栈空间的浮沉变化,OS是无能为力的。,5.3.4 指针作祟,指针通过间访改变了上层函数的数据。其实,它还可以改变任何本程序的数据。 因为编译器检查的时类型匹配,是为了杜绝编程错误,并防止越权,至于数据,只要在表达范围内,编译器不管。指针可以赋予任何值。实际上,OS的内存管理把指令和访问数据的地址都
25、管理了,一个进程只能访问自己的程序空间,不能访问其他的进程,更不能访问OS区域的数据。 通过数据指针给程序会造成破坏,如:,#include int a=5; int b=6; int main()int* ap=(int*)4202660;*ap=8;std:couta“n”;std:coutint( / 8 / 4202664,指针的无 约束性,5,6,4202660,4202664,a,b,4202660,ap,程序中,全局变量a的值通过非正规途径的访问而改变了! 只要没有指针和引用,函数就不可能去修改其他地方的数据。当指针和引用通过参数传递时,可能会给函数带来副作用,而通过const修
26、饰,可以抑制副作用。 引用也可以间访,引用一旦创建就与某个对象捆绑在一起,使得可以少犯很多错误而又不失程序的灵活性。,5.4. 函数指针 ( Function Pointers ),函数类型:函数类型因参数类型、个数和排列顺序的不同而不同,也因返回类型的不同而不同函数指针:指向代码区中函数体代码的指针.不同的函数类型,其函数指针也不同用法:函数指针经常用作函数参数,以传递连函数本身都不知道的处理过程(函数),与返回指针类型的函数不同,该函数称为指针函数。如:int *f(int a);char * copy(char *s1,char *s2);,f函数返回int指针,即int指针函数,cop
27、y函数为char指针函数,运行中的程序,其中的每个函数都存放在代码区。故每个函数都有起始地址,指向函数起始地址的指针不同于数据指针,函数指针与数据指针不能相互转换,通过函数指针可以调用所指向的函数。,5.4.1 指向函数的指针,函数指针也有不同类型,函数有多少种类型,函数指针就有多少种类型。如: void f();int k();int g(int);int h(char);int m(int,int); 都是不同类型的函数。,函数是以参数个数、参数类型、参数顺序甚至返回类型的不同来区分不同类型的。函数的类型表示时函数声明去掉函数名。 声明一个int(int)类型的函数指针gp,就是把指针名放
28、在返回类型和括号间,即:int (*gp)(int); 容纳函数指针的括号不能省,它表示*gp是一个整体,描述的是一个指针,有括号和无括号,意义完全不一样:int* gp(int ); 表示声明一个含有整数参数的整数指针函数,等价于: int* gp(int a);,定义函数指针还可以初始化,如:int g(int) ; int (*gp)(int) =g;,函数指针赋值也可以与函数指针定义分开,如:int g(int); int (*gp)(int);gp = g; 定义了一个函数指针,就有了一个指针实体,一个指针 的大小跟int型实体大小一样。,函数指针的类型必须接受编译器的检查,gp指针
29、所指向的函数拥有一个int型参数,并返回int型值。如: void f(); void (*fp)(); gp = f; /错:可疑的指针变换 gp = fp; /错:可疑的指针变换,由于函数指针本身也是一种类型,即:int (*)(int); 是int(int)型函数的指针类型,其中的“(*)”的括号时不能省的,因为时数据类型就可以作为函数参数,即: void f(int *a, int *b, int (*)(int); 所声明的f函数,含有三个参数,前两个是整型指针,第三个是函数指针。,函数指针的定义形式看起来很复杂,所以通常采用typedef简化,如:typedef int(*Fun)
30、(int a,int b); 表明声明了一个函数指针类型。注意:类型名习惯又用大写字母开头,不是定义函数指针实体,所以:int m(int,int);Fun funp=m;/okFun = m; /错:不恰当的使用typedef Fun,5.4.2 函数指针参数,数据指针除了进行参数传递外,还承接申请的存储空 间,释放空间等,而函数指针则主要是用来进行参数传 递的,就像引用一样.,函数指针作为参数传递 (函数名看作是函数指针),#include #include #include using namespace std; bool lessThanBitSum(int a, int b) in
31、t suma=0, sumb=0;for(int x=a; x; x/=10) suma += x%10;for( x=b; x; x/=10) sumb += x%10;return suma sumb; int main() int aa = 33, 61, 12, 19, 14, 71, 78, 59;sort(aa, aa+8, lessThanBitSum);for(int i=0; i8; +i)coutaai“ “;cout“n“; 结果为: / 12 14 33 61 71 19 59 78,5.4.3 函数指针数组,函数指针作为一种数据类型,也可以作为数组的元素类型。如实现要
32、用菜单来驱动函数调用的程序框架,则用函数指针数组来实现就比较容易维护,如下列程序:,#include using namespace std; /- typedef void (*MenuFun)(); void f1() coutchoice;switch(choice)case 1: fun0(); break;case 2: fun1(); break;case 3: fun2(); break;case 0: return 0;default: cout“you entered a wrong key.n“; /=,程序中首先定义了一个函数指针类型MenuFun,如果前面没有typed
33、ef,则后面部分就是一个函数指针定义,所以,正因为有了typedef,MenuFun就是函数指针的类型名,可以依次创建函数指针,但其本身并不是函数指针。 根据MenuFun创建了一个函数指针数组fun,并予以初始化,然后循环处理键盘输入。同一功能的程序,也可以用向量来实现,如下列改用向量来实现的代码:,#include #include using namespace std; /- typedef void (*MenuFun)(); void f1() cout fun(3);fun0=f1, fun1=f2, fun2=f3;for(int choice=1; choice; )cout
34、choice;if(choice0 /=,5.4.4 简略函数指针表示,int () ;是一个无名函数声明,表示一个函数类型int func();表示又一个函数类型,func是函数类型的名字int(*) (); 是一个无名函数指针int(*pf)(); 是一个函数指针定义,pf是函数指针名,函数类型是int.编程中,常用的是先定义typedef函数类型,再定义函数指针,如:,typedef int(*SIG)(); /声明返回int且无参函数的指针类型SIGtypedef void(*SIGARG); /声明无返回且无参函数的指针类型SIG signal(int , SIGARG); /声明的
35、函数返回函数指针,其参数为整型和函数指针,5.4.5 函数指针的意义,函数类型不能作为参数,如果参数给出了一个函数类型,则自动转换为函数指针,这种现象称为蜕变,如:MenuFun fun = f1,f2,f3; 本应为: MenuFun fun = ,可以同理使用函数引用:void g(); typedef void Fun();Fun,函数指针使得C+可以沟通其他语言编写的程序,如果函数指针挂接,可以将其他语言写的函数和过程引入到C+中。 函数指针使程序表现出更多的灵活性。一个函数的函数指针参数取不同值,可以让函数表现出不同的行为。 函数指针也是C+面向对象编程机制中的重要手段。,如果函数指
36、针可以越过本地进程,如果动态链接库的方式,访问共享性质的其他进程(服务器),执行其函数,甚至操作OS函数。 函数指针使得函数的意义远离黑盒的初衷,因为函数通过函数指针参数调用其他的函数,可以在更深远的范围中不可逆的改变环境,使得运算无法严格的重复运行结果。,5. 命令行与main参数,程序运行:操作系统读入命令以启动程序重定向命令:操作系统读入命令后,识别并自我消化的参数main函数参数:操作系统读入命令后,不能识别参数,将其直接传递给所启动的程序,5.5.1 命令重定向,编程归结为计算,即根据输入要求进行计算,最后获得结果,这种过程留下了输入/输出的口子,但对输入/输出设备不明确规定,视运行
37、环境而定。 如从标准的输入循环读入两个整数(直到读空),输出其和,程序代码如下:,/ 重定向输入输出 /= #include using namespace std; /- int main()for(int a,b; cinab; couta+b“n“ ); /= 结果: 8 9 17,现在改变程序在集成环境中运行的方式,从”命令提示符”窗口中输入命令f0509 ”后面规定输出设备.,发出命令 “f0509 xyz.txt”,便可以从abc.txt文件中读入数据,而将输出送到xyz.txt这中去。 运行后,屏幕中没有输出结果,而打开xyz.txt文件,则看到运行结果全在里面。,5.5.2 使
38、用main参数,重定向时基于标准输入/输出的,若命令不重定向,默认的是标准输入/输出。 编程的时候,往往不是这种单纯的情况,要处理的可能是若干个非标准设备的资源文件,也可能时遵循某种语言的命令,命令加在程序名后面构成命令行,而且该命令行可能由其他程序生成。这个时候就需要main函数的参数。,main函数的参数结构为两项参数:int main(int argc,char *argv). 在main参数表达中,要么不声明参数,要么按这里的参数格式声明,只有在定义了main函数时写出了参数项,在运行时才会接受OS的参数传递。main函数的参数由OS传递,两个形参名一般采用argc和argv,第一项表
39、示传递的C-串有几个,第二项表示具体的C-串数组,该数组最后一项是空串,即指向0的串。其参数结构见图5-8:,0,argv,argc,图5-8 main参数结构,对于以下程序,若发出命令行“f0510 abc1 abc2 abc3”,则可根据main的形参来读取命令行的相关信息:,/ 读入main参数 /= #include using namespace std; /- int main(int argc, char* argv)for(int i=0; iargc; +i)coutargviendl; /=,因此,如果命令行中为输入/输出文件名的信息,则程序可以直接读 取和创建文件流对象,
40、进行文件操作。,6. 递归函数 ( Recursive Functions ),形式上:一个正在执行的函数调用了自身(直接递归).或者,一个函数调用了另一个函数,而另一个函数却调用了本函数(间接递归)本质上:程序在运行中调用了相同代码实体的函数,却在函数栈中重新复制了该函数的整套数据,由于每套数据中的参数也许不同,导致了计算条件发生变化,使得函数得以逐步逼近终极目标而运行,5.6.1递归本质,如n!的数学函数描述为:,对应的C+函数描述为:unsigned f(unsigned n)if(n=1) return 1;return n*f(n-1); 当调用f(5)时,其运行栈描述如图5-9所示
41、:,图5-9 递归运行栈结构描述,递归有直接递归和间接递归之分。间接递归是指函数体中没有直接调用自身函数,而是调用了另一个函数,在那个函数里,出现了调用本函数的语句,或者,在那个函数里,调用了一个其他函数,反复出现调用其他函数,而最后有一个函数调用了本函数。如:,int fn1(int a) int b;b = fn2(a+1); /调用fn2 ,int fn2(int s)int c;c = fn1(s-1); /调用fn1 ,5.6.2 递归条件,递归不能无限制的调用下去,因为栈空间时有限的。 递归函数中必须有完成终极任务的语句序列,以示函数有意义。 递归函数当然有递归调用语句。然而,递归
42、调用应有参数,而且参数值应该时逐渐逼近停止条件的,限于计算机的硬件资源和性能因素,所以逼近的速度应该比较现实。 递归条件应该先测试,后递归调用。编译器不能检查出无条件递归的逻辑错误。,5.6.3 消去递归,大多数递归函数都能用非递归函数来替代。如:,long gcd1(int a, int b) / 递归版 if(a%b=0)return b;return gcd1(b, a%b); /- long gcd2(int a, int b) / 非递归版 for(int temp; b; a=b, b=temp)temp = a%b;return a; /-,5.6.4 递归评说,递归的目的是简化
43、程序设计,使程序易读。对于一个能够写出递归通项的数学函数,先将其程序化,然后考虑一些边界条件,就能达到目的。但递归也会迅速递增系统开销,时间上,执行函数的调用与返回的次数明显要大于非递归函数,空间上,栈空间资源也会遭到空前的劫掠,每递归依次,栈内存就会多占用一截。递归函数在时空开销上的不利局面势必影响性能。,5.7.1 重载概念,C+编译器能够根据函数参数的类型、数量和排列顺序的差异,来区分同名函数,其技术称为重载技术,相应的同名函数称为重载函数。如:int abs(int); /求整数的绝对值double fabs(double);/ 求浮点数的绝对值 针对上述函数声明,可以有如下表述:,/
44、 函数重载 /= #include /- int abs(int a)return (a0)? a : -a; /- double abs(double a)return (a0)? a : -a; /- int main()std:coutabs(-10)“n“;std:coutabs(-12.23)“n“; /=,结果: 10 12.23,重载的目的时通过自然描述函数名称来简化编程和增强程序的可读性。它让程序员方便的将特定的操作序列与一个自然的名称相对应,因此,可以很容易地分辨出滥用重载的不良编程。如:,int abs(int a );double abs(double a);void f
45、unc(int a ,double d)int b =abs(a);double c = 1.0+abs(d) ;/错: 将abs的功能误解为求绝对值,5.7.2 重载函数匹配,只要参数个数不同,参数类型不同,参数顺序不同,函数就可以重载。然而,返回类型不同则不允许函数重载。如: void func(int a); void func(char a); void func(char a,int b); void func(int a,char b); char func(int a); /错:与第一个函数冲突 因为调用函数是只看参数匹配的,有的函数虽然只有返回 类型,但不参与表达式运算而单独作
46、为一条语句,因此函 数的匹配不能根据上下文来判断。,C+按下列三个步骤的先后顺序找到匹配并调用函数: 寻找一个严格匹配,如果找到了,就用那个函数。 通过相容类型的隐式转换寻求一个匹配,如果找到了,就用那个函数。 通过用户定义的转换寻求一个匹配,若能查出有惟一的一组转换,就用那个函数。,C+允许int型到long型,int型到double型的隐式转换,但若必须在两者之间抉择时,则会引起错误。如:void print(long a);void print(double a);void func(int a)print(a) ; /错:long型与double型匹配哪一个呢? 为避免二义性,在调用时
47、,应显式表明是print(long a)还是print(double a). 如print(long)a);,5.7.3 重载技术,C+用名称压轧技术来改变函数名,区分参数不同的同名函数。一系列代码被附加到函数名上以标记参数类型以及它们出现的次序。如用v、c、i、f、l、d、r分别表示void、char 、int 、float 、double以及其引用,则重载函数:int func(char a);int func(char a,int b,double c); 在编译器内部分别被表示为func_c和func_cid. 名称压轧是在编程过程中悄悄进行的,在链接过程中默默使用,它不是C+标准,所以各个编译器的名称压轧方案可能不同。,