第二部分 过程化编程 Part Ⅱ The Procedural Programming 第5章 函数机制(Function Mechanism)                                          早在C语言中,就确立了函数在程序设计中的地位。函数是C也是C++程序结构中的基本单位,只不过C++函数在内涵上对C进行了诸多扩充。从结构上或者从本质上说,设计程序就是设计函数。从最初的main函数启动,到程序运行结束,无不是函数在起作用。函数的不同组织方式就形成了不同的程序设计方法。在数据还没有太复杂、太混乱、太庞大之前,玩弄函数的设计,“大了分,小了合”,就是基于函数的程序设计了。当然,在C++中,函数与过程的相似性在概念上大可以玩一些文字游戏,但到了5.3节看透了函数之后,对C++函数的本质应不会有什么悬念了。   也就是说,是函数构建了C或C++程序。C++的函数其实是一个过程体,有些函数具有输入参数和返回类型,表现得像数学函数那样,有自变量和函数值,甚至还可以对其进行递归描述;另外一些没有返回类型,没有调用的数值结果,“什么也没有得到”,表现得像一个纯粹的过程。在C++中,函数的作用并不只是为了对于给定的数据,计算后返回一个函数值那样完成一个预定的目标。C++语言的设计,其函数参数的意义并不仅仅是一个类型的变量值,很多是指向数据流的一个窗口,或者数据集合以及一组相关操作,即对象。完成了一个过程,也就完成了数据集合的处理。所以C++中的函数可以涵盖一切数据操作过程,从这个意义上说,C++的函数是涵盖更广的过程,基于函数的程序设计就是基于过程的程序设计。   正因为函数在C++中是如此重要,所以函数的使用也必须十分规范。函数使用的规范性体现在函数参数的性质上,体现在对待函数参数的传递规则和返回类型与返回值的审查上,体现在函数名字的识别原则上,体现在函数体编译的效率选择上,还体现在函数体对数据的访问权限上。                      5.1 函数性质(Function Character) ? 5.1.1 函数的形态(The Function Forms)   数学函数描述自变量与因变量之间的多对一的数量关系,还研究其各种性质,包括可逆性。在实际应用中,强调求值趋向,且没有任何副作用,结果亦可重复计算获得。而C++函数描述操作序列,虽有求值表现,但更强调过程性,因而就强调构建过程的结构框架,即模块结构。在语言设计的时候,套用了函数的原始意义——求值,但却与函数的范畴相去甚远。它可以表现为数学意义上的函数,也可以表现为纯粹的过程操作,更可以表现为结果的多元化描述。由于它可以通过模块结构对过程进行分解,所以实际上具备了描述数学模型的条件,它甚至还可以有副作用,有意无意地让结果不可重复,以使函数的设计带有高度的技巧性和灵活性。那么,它在形式上是如何表现的呢?   有些函数具有求值表现,即有返回值。这些函数可以没有输入参数,也可以有一个或者多个。例如,数学函数为下列形式:    其等价的C++函数描述为:    long double f(long double x){ if(x < 0) return -x; if(x > 0) return x*x; return 2; }      不过要注意的是,数学函数的自变量定义域为±∞,而C++函数中的long double型参数表示范围为±1.2×104932。事实上,因为值域也为long double型,所以参数的表示范围在大于0时为函数值的平方根:   –1.2×104932≤?x?≤1.1×102466   当然还要注意,实数与long double型数在精度上存在着差异。这些都是编程语言在描述能力上的局限性造成的。   函数还可以进行递归描述(?CH5.6)。   函数可以没有参数或者有多于一个的参数。例如,求随机数的函数“int rand();”没有参数,但可以获得非负的整数范围内的随机数。该函数属于C库函数,它取系统中的当前时间与内部自身的一个素数加工成一个伪随机数,作为函数值。而求矩形面积的函数“double area(double width, double length);”显然有长度和宽度两个double型参数,并返回double型值,其为典型的多对一f(a,b)形式的数学函数。   另外一些函数没有“求值”表现,没有返回值,即返回类型用void描述。调用的结果“什么也没有得到”,表现得像个纯粹的过程。例如,输出一些字串的函数为:    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; im? m:n)*10; //保证运输次数最少 }      函数总有各种各样的实现,就好像人有各种各样的差异一样。所调用的函数性能越好,程序的总体质量也越高。C++基本库中的函数都是高度精练的,所以能调用C++库中的函数就尽量调用,库中没有的函数才自己手写,这样可以减少程序员自己的编程量,而且总体质量也高。 ? 5.1.3 传值参数(Value-Passed Parameters)   函数通过参数传递输入数据,参数通过传值机制实现。所谓传值,即在函数被调用之时,用克隆实参的办法创建形参。例如:    void f(Type a); //Type为任意数据类型,a为形参 void g(){ Type x; f(x); //此处x为实参,相当于定义"Type a = x;" }      克隆实参就是用实参创建形参而实参本身没有任何改变。   当上面的x的类型与a的类型不一致时,就要看两个类型是否相容,如果相容,那么,在调用时,会将类型作隐性转换而完成传值;如果不相容,则作为编译错误而绝不妥协。这种类型匹配的检查,给程序员带来的是福音,可以让程序员早早地发现潜在的错误。例如:    struct S{int a,b;}; void f1(S a); void f2(int b); void f3(int* c); void f4(int& d); //--------------------------- void h(){ S x = {1,2}; f1(x); //ok: S a=x; f1(2); //错: S a=2;类型不匹配 f2(23); //ok: int b=23; f2(2.5); //ok: int b=int(2.5); //类型转换,精度受影响 f2(x); //错: int b=x;类型不匹配 int e = 3; f3(&e); //ok: int *c=&e; f3(e); //错: int* c=e;类型不匹配 f4(e); //ok: int& d=e; f4(&e); //错: int& d=&e;类型不匹配 }      函数是一个计算单位,它可以返回也可以不返回值完成一个计算。有许多任务光靠复制几个参数数据做输入是不够的,而且数据还要返回。返回总是只有一个数据项。例如,输入数据为100个整数,让其排序输出。总不至于用100个参数吧,不敢想象下面这样的函数声明:    void sort(int a001,int a002,int a003,...,int a100);      就算可以,那1000个、10 000个整数的排序呢?何况,初级的排序操作需要线性存储结构的下标操作,如向量。那么,如果传递给函数一个数据整体的复制品呢?例如:    vector mySort(vector v); void f(){ int a[]={3,5,7,1,8,4,9,2,6,0}; vectot va(a,a+10); vector vb = mySort(va); //vector v=va; ... }      那也不行!函数确实可以执行mySort排序任务,但那是施加在数据复制品上的操作。要完成任务,还必须将这些数据返回,这大批数据随着函数调用一来一往,函数工作的效率马上降低,见图5-2,那就不是C++的套路了。 图5-2 大数据流量的函数   至于数组,还不能实现这种传输呢,因为它没有数据整体复制能力,只能通过指针传递(?CH5.2.1)。   诚然,该方法可以实现数据的排序。但翻遍C++编程方法的经典书,你不会找到用这种方法编程的,除非是想测试用这种方法所带来的负面效应是多少。 5.2 指针参数(Pointer Parameters) ? 5.2.1 指针和引用参数(Pointer & Reference Parameters)   例5-1中有效的方法是传递给mySort排序函数以一个数组指针或者容器引用。这样,传递的是指针和引用值,也就无须大块数据的挪动了。然后,通过指针和引用的间接访问实现数据的操作。这时候,其实是赋予了函数去操作“异地”数据的权力。异地指非本函数数据区或者称非栈顶函数区(?CH5.3)。为了完成计算任务,这个权利是必要的。因为这个原因,函数可以大言不惭地说,可以高效地完成数据操作任务了。而且正因为函数的这种能力,才使得C++程序奠定了以函数为基石的结构。进而,函数作为过程的拓展,基于函数的程序设计就称为基于过程的程序设计了。正因为这个权利,函数调用才这么灵活、高效,无须承受大块数据传递的沉重负担。例如,传递数组的方法:    void mySort(int* b, int size); void f(){ int a[] = {3, 5, 7, 1, 8, 4, 9}; mySort(a, sizeof(a)/sizeof(a[0])); //int* b=a; //... }      数组是不能整体直接复制的,即    int a[10]; int b[10] = a; //错 b = a; //错      数组只能通过传递数组起始地址,达到使用该数组的目的。正因为如此,数组作为函数参数传递,实际上只是把数组的起始地址传给了函数,因此为了能有效地使用数组,还必须给数组传递一个元素个数。在f函数中,创建了数组a,将其传递给mySort函数。由于知道传递的数组名只是一个数组的起始地址,因此,mySort的形式参数有两个。   函数的数组传递的实质,是数组地址的克隆。当初,C语言在设计的时候,看到了数据传递实在没有传递整个数组空间的必要,因而弄了一下巧,结果其效率享誉了全世界。就好像我要让你用车,何必把车扛过来呢,太重啊:(,只需将钥匙交给你就得了。   然而,这种编程方法,还是请不要学,至少是初学时不要学,因为它不是理想的编程方法,一维数组的传递有一些褒贬不一的说法,但二维数组的传递绝对是一边倒的共识。你可能会看到一些C或C++语言的书对多维数组的描述(二维以上的数组,称为多维数组),若真的胆敢传递多维数组给函数,那么,一切美感将会消失殆尽。所以,等到琢磨透了编程,成为了高手,再研究这样的编程手法吧。   然而,传递指针和引用的特性作为一把双刃剑还是被专业程序员所看好。本质上,初级程序员与专业程序员的差别就可以从使用指针和引用参数中看出。由于参数值可以通过传递指针和引用获得,所以它意味着函数可以访问不属于自己管辖的数据,如图5-3所示。   很显然,只传递数组的地址,明显减轻了参数传递过程中的数据复制量。但是要看清,排序是在数组a上操作的,数据的改变不是在mySort函数数据区内,等到mySort返回之际,数组a的内容发生了改变,而参数b本身的值却没有发生任何改变(还是指向数组a)。这便是函数运行的结果!   这意味着函数运行的结果并不一定要用返回类型规定的返回值反映,函数参数也能起到返回结果的作用。因为最终,函数结果要返回到调用函数中,而函数的指针和引用参数,指示着调用函数的数据内容已经发生变化,这不就是运行所要达到的目的吗!?   所以,函数可以传递多个输入参数,同时也可以让多个数据处理的对象发生变化。输出因而也可以呈多元化而非返回值一个形式。 图5-3 数组传递   例5-2 在一个矩阵中找含有–1元素的向量,若找到,则找到的结果存放在参数v中,返回true;否则,没有结果,且返回false。程序所用的数据放在文件abc.in中。文件中第1行放向量的个数n,以便读入操作。接下来n行即n个长短不一的向量。程序代码如下:    (1) //===================================== (2) //f0501.cpp (3) //向量参数传递 (4) //===================================== (5) #include (6) #include (7) #include (8) #include (9) using namespace std; (10) //---------------------------------- (11) typedef vector VI; (12) typedef vector VVI; (13) void print(const VI&); (14) void input(VVI&); (15) bool findVec(const VVI&, VI&); (16) //------------------------------------- (17) int main(){ (18) VVI matrix; (19) input(matrix); (20) VI vec; (21) if(findVec(matrix, vec)) (22) print(vec); (23) }//------------------------------------ (24) void print(const VI& v){ (25) for(int i=0; i>n; (32) m.resize(n); (33) for(string s; n–- && getline(in, s); ) (34) for(istringstream sin(s); sin>>t; m[m.size()-n-1].push_back(t)); (35) }//------------------------------------ (36) bool findVec(const VVI& matrix, VI& v){ (37) for(int i=0; i (6) #include (7) using namespace std; (8) //------------------------------------- (9) void print(vector& a){ (10) for(int i=0; i add(vector& a, vector& b){ //用a存放结果 (15) for(int i=0; i a(aa,aa+7), b(bb,bb+7); //复制数组给向量 (22) vector c = add(a, b); (23) print(a); print(b); print(c); //查验向量内容 (24) }//====================================         程序是将两个初始化了的数组赋给两个向量a和b,然后调用add相加,并输出这两个向量以及结果向量。设计函数是很机械的,只要输入数据,按照功能要求、性能要求,满足它就行,本例中的add函数就是一个典型的例子。说实在话,设计者还为自己第16行的“+=”操作符而得意呢,因为针对对象的操作的效率不差于“a=a+b”,而且,设计中简化了操作,不建临时向量,一切以效率为第一要素,只要完成计算就行的思想贯彻得很好。   不过,main函数可傻眼了,它调用的add函数结果虽然对了,但是原始数据的向量a却被破坏了。这就是函数运行所带来的副作用!就好像心脏病患者,吃了药之后,心脏病控制住了,但药物所带来的副作用,使关节炎却意外地发作了:)。   独立性是C++对函数的定位,可能设计函数者远在天边,通过Internet与软件工程师(程序设计的规划者)保持联系。软件工程师与程序员的沟通不可能在调用add函数的环境上。事实上,调用环境是千变万化的。软件工程师也不可能对add函数的设计指手画脚,就像前面的比喻:妈妈叫儿子买酱油般的唠叨。软件工程师只能通过函数声明和相关的性能要求描述,框定函数设计要求。而程序员自有一片任意发挥的天空,只要满足了软件工程师的要求就行。   问题出在规定函数参数传递的声明上,传递引用给了函数超限的权力,函数循着传递的引用名既读又写地访问了引用的空间,而那一片引用空间并不是函数所拥有的。因此,C++语言的限制手段就是在指针和引用参数上加const修饰,以此限制函数体中对参数的写操作。即    vector add(const vector& a, const vector& b){ for(int i=0; i add(const vector& a, const vector& b){ vector c(a); for(int i=0; i add(const vector& a, const vector& b){ vector c(a.size()); for(int i=0; i add(const vector& a, const vector& b){ vector c; for(int i=0; i using namespace std; //------------------------------------- void f(){ int b; cout<<"B=>"<"< using namespace std; //------------------------------------- int a=5; int b=6; //------------------------------------- int main(){ int* ap=(int*)4202660; *ap=8; cout< aa(a, a+8); sort(aa.begin(), aa.end());      但若整数的大小是以各位数字之和的大小决定的,则就不能直接使用sort函数排序。需要先定义一个比较函数,然后再对sort传递比较函数指针,以让sort知道大小关系不是默认的整数值比较,而是根据比较函数判定。可用函数指针调取比较函数进行排序工作。代码如下:    (1) //===================================== (2) //f0506.cpp (3) //函数指针传递 (4) //===================================== (5) #include (6) #include (7) #include (8) using namespace std; (9) //------------------------------------- (10) int bitSum(int a); (11) bool lessThanBitSum(int a, int b){ return bitSum(a) aa(a, a+8); (16) sort(aa.begin(), aa.end(), lessThanBitSum); //函数名即函数指针 (17) for(int i=0; i (6) using namespace std; (7) //------------------------------------- (8) typedef void (*MenuFun)(); //函数指针类型名称 (9) void f1(){ cout<<"good!\n"; } (10) void f2(){ cout<<"better!\n"; } (11) void f3(){ cout<<"best!\n"; } (12) //------------------------------------- (13) int main(){ (14) MenuFun fun[]={f1,f2,f3}; //函数指针数组 (15) for(int choice=1; choice;){ (16) cout<<"1-----display good\n" (17) <<"2-----display better\n" (18) <<"3-----display best\n" (19) <<"0-----exit\n" (20) <<"Enter your choice:"; (21) cin>>choice; (22) switch(choice){ (23) case 1: fun[0](); break; (24) case 2: fun[1](); break; (25) case 3: fun[2](); break; (26) case 0: return 0; (27) default: cout<<"you entered a wrong key.\n"; (28) } (29) } (30) }//====================================      程序中(第8行)首先定义了一个函数指针类型MenuFun。如果前面没有typedef,则后面部分就是一个函数指针定义,所以,正因为有了typedef,MenuFun就是函数指针的类型名,可以依此创建函数指针,但其本身并不是函数指针。   在第14行,根据MenuFun创建了一个函数指针数组fun,并予以初始化。初始化的每个值都是同类型的函数名,它们在第9、10、11行定义。接下来就是循环处理键盘输入,每当输入值为1、2或3时,显示good、better或best等字样。同一功能的程序也可以用向量来实现,以下是改用向量实现的代码:    (1) //===================================== (2) //f0508.cpp (3) //函数指针向量 (4) //===================================== (5) #include (6) #include (7) using namespace std; (8) //------------------------------------- (9) typedef void(*MenuFun)(); (10) void f1(){ cout<<"good!\n"; } (11) void f2(){ cout<<"better!\n"; } (12) void f3(){ cout<<"best!\n"; } (13) //------------------------------------- (14) int main(){ (15) vector fun(3); (16) fun[0]=f1, fun[1]=f2, fun[2]=f3; (17) for(int choice=1; choice;){ (18) cout<<"1-----display good\n" (19) <<"2-----display better\n" (20) <<"3-----display best\n" (21) <<"0-----exit\n" (22) <<"Enter your choice:"; (23) cin>>choice; (24) if(choice>0 && choice<4) fun[i-1](); (25) else if(choice==0) return 0; (26) else cout<<"you entered a wrong key.\n"; (27 } (28) }//==================================== ? 5.4.4 简略函数指针表示(The Outline of Function Pointers)    无名函数与无名函数指针   C++语句:    int ();    是一个无名函数声明,表示一个函数类型。    int func();    也是一个函数声明,表示又一个函数类型,其中func是函数的名字,凭此名字可以推断其函数类型。    int (*)();    是一个无名函数指针;    int (*pf)();    是一个函数指针定义,pf是函数指针名,该指针将会指向类型是int()的函数。    typedef函数类型 typedef int Func(); //将该函数类型int()定义为Func    则上面的函数指针可等价定义为:    Func* pf; //不能写为int ()* pf;    函数指针的函数(函数指针函数)声明 int (*func(int))();    是一个指针函数声明,即指针函数*func(int)的返回类型是int()。也就是:    Func* func(int); //函数指针函数func      由于在定义函数指针的时候,总是要“穿插”在函数类型中,所以编程中,更多的是先定义typedef函数类型,再定义函数指针。例如:    typedef int(*SIG)(); //指向int函数的指针类型SIG typedef void(*SIGARG)(); //指向void()函数的指针类型SIGARG SIG signal(int, SIGARG); //声明一个函数,其返回SIG,其第二参数为SIGARG ? 5.4.5 函数指针的意义(The Sense of Function Pointers)   (1)C语言处理类型不严密,C++必须改变。就像数组参数只能传递以指针,函数参数也只能传递以函数指针。如果参数中给出了一个函数类型,则自动转换为函数指针。这种现象称为蜕变(decay)。程序f0507.cpp中的语句:    MenuFun fun[] = {f1, f2, f3};    就是因为蜕变才有这样的形式,本应为:    MenuFun fun[] = {&f1, &f2, &f3};    也使得函数指针调用函数的形式成为:    fun[0](); //应理解为*fun[0]();      (2)可以同理使用函数引用:    void g(); typedef void Fun(); Fun& f = g; f();      (3)函数指针使得C++可以沟通其他语言编写的程序,通过函数指针挂接,方便地将其他语言写就的函数和过程引入C++中来。   (4)函数指针使程序表现出更多的灵活性。一个函数的函数指针参数取不同值,就可以让该函数引用不同的函数,表现出不同的行为,如程序f0506.cpp中的sort可以用不同的比较函数规定元素的大小规则。   (5)函数指针也是C++面向对象编程机制中的重要手段,多态实现的动态绑定技术就是基于函数指针(?CH12.3.2)。   (6)事实上,因为函数代码是跨进程的,所以通过函数指针可以越过本地进程,通过动态链接库(?CH13.5.1)的方式,访问共享性质的其他进程(服务器),执行其函数,甚至操作系统函数。这些内容涉及高级编程。   (7)函数指针使得函数的副作用更为恶劣,函数的意义更远离黑盒的初衷,使得函数设计更“变幻莫测”。因为函数通过函数指针参数调用其他的函数,可以在更深远的范围中不可逆地改变环境,使得运算无法严格地重复运行结果。 5.5 main函数参数(The main’s Arguments)   编好了程序,要投入应用,就要启动程序。程序运行是在操作系统环境下进行的。操作系统对所启动的程序也提供一些服务,接受程序启动时的临时输入/输出重定向请求,或者干脆把用户的请求通过参数传递给程序,让程序处理运行要求。 ? 5.5.1 命令行重定向(Redirecting Command Line)   各种编程在许多情况下都归结为计算,也就是说,根据输入要求进行计算,最后获得结果。这种强调计算的过程,虽然留下了输入/输出的口子,但往往对输入/输出设备并不明确规定,而是视运行的环境而定。因此,一些程序就灵活地用可重定向的标准输入/输出设备担当此任务。   例如,从标准输入设备循环读入两个整数(直到读空),输出其和。程序代码如下:    //===================================== //f0509.cpp //重定向输入输出 //===================================== #include using namespace std; //------------------------------------- int main(){ for(int a,b; cin>>a>>b; cout<”后面规定输出设备。例如,通过发出命令“f0509 xyz.txt”,便可以从abc.txt文件中读入数据,而将输出送到xyz.txt中去:             abc.txt xyz.txt         运行后,屏幕中没有输出结果,而打开xyz.txt文件,则看到运行结果全在里面。 还可以通过将“>”改为“>>”让输出结果追加到文件中去。读者可以自行验证。   ? 5.5.2 使用main参数(Using Main Arguments)   重定向是基于标准输入/输出的,也就是说,命令若不重定向,则默认的是标准输 入/输出。   在编程的时候,往往不是这种单纯的情况,要处理的可能是若干个非标准设备的资源文件,也可能是遵循某种语言的命令,这个命令加在程序名后面构成命令行,而且该命令行可能由其他程序生成。这时候,重定向就不行了,用简单的标准输入会话形式输入命令也不行了,需要依赖main函数的参数。   main函数的参数结构为两项参数:    int main(int argc, char** argv){ ... }      最早的时候,还有第三项参数形式,那是为了设置运行环境的,现在因为操作系统的不断进化,甚至在Java中也很少用到这项参数。故我们看到的是两个参数的main形式。   在main参数表达中,要么不声明参数,像以前一样;要么按这里的参数格式声明。只有在定义main函数时写出了参数项,在运行时才会接受操作系统的参数传递。   main函数的参数由操作系统传递,所以比较特殊。两个形参名一般是采用习惯名称argc和argv,表示argument count和argument vector,即第一项是表示传递的C-串有几个,第二项是表示具体的C-串数组,该数组最后一项是空串,即指向0的串。正像在函数中传递数组那样,既要传递数组地址,也要传递数组的元素个数。要注意的是,C-串的类型为char*,数组是以指向C-串的指针为元素的,因而数组描述为char**。其参数结构的示意图见图5-8。 图5-8 main参数结构   对于以下程序,若发出命令行“f0510 abc1 abc2 abc3”,则可以根据main的形参读取命令行的相关信息: //===================================== //f0510.cpp //读入main参数 //===================================== #include using namespace std; //------------------------------------- int main(int argc, char** argv){ for(int i=0; i #include using namespace std; //------------------------------------- int main(int argc, char** argv){ if(argc!=3) cout<<"Usage: f0511 infile outfile\n"; else{ ifstream in(argv[1]); ofstream out(argv[2]); if(in && out) out< #include using namespace std; //------------------------------------- int main(int argc, char** argv){ if(argc!=2){ cout<<"usage: f0512 command\n"; return 1; } string s(argv[1]); istringstream sin(s); //用string串流来解析命令 int a,b; char c; sin>>a>>c>>b; switch(c){ case'-': b=-b; case'+': cout<f0512 23+ E:\ch05>f0512 23+* E:\ch05>f0512 23+0 E:\ch05>f0512 *23+    等错误,如何辨认?   要做这种命令式处理,首先要定义语法规则及其操作,即一种语言。可以将其做成一个计算器(?参考文献[1],CH10.2)。 5.6 递归函数(Recursive Functions) ? 5.6.1 递归本质(Essence of Recursions)   递归函数即在函数体中出现调用自身的函数。例如,阶乘n!的数学函数描述为:    其对应的C++函数描述为:    unsigned f(unsigned n){ if(n==1) return 1; return n*f(n-1); }      不过要注意的是,f?(13)>232。所以,对于unsigned int型(无符号类型)的表示范围,其n的取值范围只有1≤?n?≤12了。   又如,Fibonacci数列的数学函数描述为: 其等价的C++函数为:    unsigned int f(unsigned int n){ if(n==0 || n==1) return n; return f(n-1)+f(n-2); }      这里要注意n的表示范围与数学函数的差异。由于C++函数f?(47)>232,超过了无符号整型数的表示范围,所以该C++函数参数的范围取值为1≤n≤46。   递归有直接递归和间接递归之分。所谓间接递归,是指函数体中没有直接调用自身函数,而是调用了另一个函数,在那个函数里出现了调用本函数的语句;或者,在那个函数里又调用了一个其他函数,反复出现调用其他函数,而最后有一个函数调用了本函数。例如:    int fn1(int a){ int b; b = fn2(a+1); //调用 fn2 } int fn2(int s){ int c; c = fn1(s-1); //调用 fn1 }      由于函数的独立性,即一个函数从来不从属于另一个函数,所以任何函数内不能嵌套定义其他函数,也即不存在子函数的概念。   函数调用时,被调函数中保护了调用函数的状态,包括硬件运行状态、返回地址和数据环境,使得调用函数的状态可以在被调函数返回时全面恢复。恢复与被调函数的状态是无关的。函数的调用在形式上只与函数的参数和返回类型发生关系。   递归函数在运行时,被调函数的数据环境与调用函数数据环境在结构上是一致的。只是由于被调函数与调用函数传递的参数值不同,才使数据环境显出差异。数据环境的操作统一由栈机制实施。栈操作总是处理位于栈顶的函数数据区,函数调用的嵌套深度体现在“货栈”的高度,而栈中不同层次的数据彼此之间在理论上被看作是无关的。   递归函数在运行中,其调用与被调函数的指令代码是同一个函数副本,只不过各个不同运行中的调用点,作为状态的一部分,在栈中被分别保护了起来。因此,是C++的函数机制决定了递归操作的可能性与形式。   例如,上述n!的函数,当调用f?(5)时,其运行栈描述如图5-9所示。    f?(1) n = 1 返回值1 f?(2) n = 2 返回值2×f?(1) f?(3) n = 3 返回值3×f?(2) f?(4) n = 4 返回值4×f?(3) f?(5) n = 5 返回值5×f?(4) 图5-9 递归运行栈结构描述 ? 5.6.2 递归条件(Condition of Recursions)   递归函数设计往往是从数学上的递推关系导出,而递推关系都是有数值规定的,这就给出了递归函数的条件执行性。   (1)递归不能无限制地调用下去,因为栈空间是有限的。所以递归函数是有条件地调用自身,该条件就是递归的停止条件。例如,阶乘函数中的“if(n==1) return 1;”当n为1时,函数就不再递归了。   (2)递归函数中必须有完成终极任务的语句序列,以使函数有意义。例如,“return 1;”就是阶乘函数完成终极计算的语句,而递归调用并非终极。   (3)递归函数当然有递归调用语句。然而,递归调用应有参数,而且参数值应该是逐渐逼近停止条件的。例如,阶乘函数中的递归调用f?(?n–1)对于f?(n)来说,是逐渐逼近了停止条件。限于计算机的硬件资源和性能因素,递归调用的嵌套深度实在有限,所以逼近的速度应该比较现实。例如,若为了做一个1~n的求和计算,用递归就很傻,因为逼近的速度实在不敢恭维:    int sigma(int n){ if(n==1) return 1; return sigma(n-1)+n; } //------------------------      (4)递归条件应先测试,后递归调用。无条件递归的逻辑错误编译器是检查不出来的,要靠程序员自己把握。例如:    void count(int val){ count(val-1); //无尽递归 if(val>1) //无法到达此处 cout<<"ok:"< x; //需要包含stack头文件 x.push(a); x.push(b); while(1){ int y=x.front(); //y<––b x.pop(); int z=x.front(); //z<––a x.pop(); if(z%y==0) return y; x.push(y); x.push(z%y); } }//--------------------------      gcd1为递归函数,gcd2和gcd3为非递归函数,gcd2为简捷版,gcd3为用栈消去递归之一般方法,自动非递归转换就依赖于这种形式化的方法。 ? 5.6.4 递归评说(Comment on Recursions)   递归的目的是简化程序设计,使程序易读。对于一个能够写出递归通项的数学函数,我们总是很容易将其程序化,然后考虑一些边界条件,就可以设计成递归函数。例如,前述的Fibonacci数列。但递归也会迅速递增系统开销,在时间上,执行函数的调用与返回的次数明显要大于非递归函数;在空间上,栈空间资源也会遭到空前的劫掠,随着每递归一次,栈内存就会多占用一截。递归函数在时空开销上的不利局面势必影响性能(?CH6.3)。   非递归函数虽然效率高,但有时候却比较难编程,而且相对来说可读性差。   现代编程在层次上做了很多分工,低级编程强调性能(?CH6.7),抽象编程(相对较高层次)强调可读(?CH11.1)。就好像车间工人以产品质量为考核依据,而公司经理以经营战略论英雄成败。这些层次,随着计算机硬件性能的不断提高,随着软件方法的不断改善,还将进一步深化。所以,作为对特殊问题的一种处理方法,递归仍然占有编程的一席之地。许多高难算法的简捷描述,往往采用递归,况且递归在迅速退出栈结构的技术处理上还能受到异常机制的意外帮助(?CH15.7.2)。 5.7 函数重载(Function Overloading) ? 5.7.1 重载概念(Concept of Function Overload)   最基本的编程就是给变量和函数命名,在函数中编写一组操作,再以一定方式组织函数。请设想一下在一个程序中,调用函数的数量像语句那么多的时候(面向对象程序中调用的函数真有那么多),其函数命名的难度就开始显现。因为函数多为全局的,彼此都能看到,所以不能重名。对于C++这样的产业化编程工具来说,命名问题确实是个实际问题,然而,C++的名空间机制(?CH7.6)也确实妥善地处理了这个问题。   另有一类问题,是对于一组概念相同、处理对象不同的函数(在C中只能归为不同的函数)。例如,求绝对值函数: int abs(int); //求整数的绝对值 double fabs(double); //求浮点数的绝对值      希望通过函数重名达到简化编程的目的,这也是C++致力于简化编程之处。   生活中,人们习惯于区分不同场景下的同名操作。例如,指令“open the door”,在冰箱面前指的是开冰箱门,在轿车面前则指开车门。上面两个函数声明,同样是求绝对值,却要说明成“求整数绝对值(abs)”“求双精度数绝对值(fabs)”,对于这两个函数的参数的不同却置若罔闻,这实在是不必要的。事实上,从声明中提供的信息来说,参数类型已经足够描述所要求的操作性质。C++完全可以同时定义下列函数而不会引起函数意义上的冲突:    int abs(int); double abs(double);      剩下的就是技术上的处理了。C++编译器事实上也能够根据函数参数的类型、数量和排列顺序的差异,区分同名函数,其技术称为重载技术,相应的同名函数称为重载函数。重载技术化解了一部分名称命名问题,然而更重要的是在编程逻辑上,亲近了人类的自然思维,从而方便了表达。   例如,针对上述函数声明,可以如下表述:    //===================================== //f0513.cpp //函数重载 //===================================== #include //#include //------------------------------------- int abs(int a){ return (a>0)? a : -a; }//------------------------------------ double abs(double a){ return (a>0)? a : -a; }//------------------------------------ int main(){ std::cout< using namespace std; //------------------------------------- void delay(int a = 2); //函数声明时 //------------------------------------- int main(){ cout<<"delay 2 sec....."; delay(); //等于调用delay(2) cout<<"ended.\n"; cout<<"delay 5 sec....."; delay(5); cout<<"ended.\n"; }//------------------------------------ void delay(int a){ //函数定义时 int sum=0; for(int i=1; i<=a; ++i) for(int j=1; j<3500; ++j) for(int k=1; k<100000; ++k) sum++; }//====================================      delay(延时)函数有一个参数,表示延迟的时间,如果没有函数声明中对参数的默认值描述,则调用的形式必须带一个参数以确定延迟时间。但真要这样就缺少了一些灵活性,因为对于delay函数,可能大部分情况都需要延迟2秒,在特殊情况时,才要延迟更多的秒数。于是,大多数情况下,delay函数调用是固定的2秒形式,就像生活中经常碰到的“老时间、老地点、老方法”之类,而无须重新精确描述大家都清楚的具体时间、地点和方法。而且如果程序进行必要的维护时,如参数值的单位改成毫秒,则参数值将从2变成2000,这时带有默认参数值的函数调用就可以免于调整,而其他没有默认参数的函数都需要进行修改。所以,从方便编程的角度看,默认参数形式带来了诸多便利。   况且在编程中,默认参数还可以避免调用中实参描述上的不统一。例如:    delay(2); //文字量实参 int a=2; delay(a); //变量实参 const int b=2; delay(b); //常量实参      这些不统一会给严格类型对等匹配的模板带来一些麻烦(?CH14.2.1)。 ? 5.7.5 默认参数规则(Default Parameter Rules)   一般来说,默认参数值总是在函数声明时描述。因为实用的程序中,函数声明总是与函数定义分离的,而在又有声明又有定义时,默认参数值只能置身于声明中。例如:    void point(int =3, int =4); //默认参数值,同时注意可以形参省略 void point(int x, int y){ //定义中不允许再给出默认值 cout< #include using namespace std; //------------------------------------- vector b(10, 0); void print(const vector& a = b); bool process(vector& a); //------------------------------------- int main(){ vector a(10, 5); //... if(process(a)) print(a); else print(); }//------------------------------------ void print(const vector& a){ if(a==vector(10, 0)){ cout<<"failed.\n"; return; } for(int i=0; i& a){ int sum = 0; for(int i=0; i100) return true; else return false; }//====================================      该程序做的工作是通过调用process函数,判断是调用print(a)还是调用print()。两种调用虽然都是输出工作,但内容截然不同。所以还不如用两个函数描述两种操作为妥。程序f0515.cpp中的print函数用了默认参数值,该参数值决定了函数中的不同操作,是输出failed还是输出其整个向量,这比起将两部分代码分开成独立的重载函数效率要低一些。更重要的是,从程序的逻辑性和可维护性来说,重载函数也更优些。重载函数版本见下面的f0516.cpp程序代码:    //===================================== //f0516.cpp //重载函数版本 //===================================== #include #include using namespace std; //------------------------------------- void print(const vector& a); void print(); bool process(vector& a); //------------------------------------- int main(){ vector a(10, 5); //... if(process(a)) print(a); else print(); }//------------------------------------ void print(){ cout<<"failed.\n"; }//------------------------------------ void print(const vector& a){ for(int i=0; i& a){ int sum = 0; for(int i=0; i100) return true; else return false; }//==================================== 5.8 目的归纳(Conclusion)   函数是程序的基本组成。它不仅是数学意义上定义域到值域的映射,而且是灵活多样的过程。本质上基于过程的程序设计就是以函数作为基本过程单位的。   然而,仅以函数组成程序是远远不够的,没有数据结构架构,在大型程序设计中,面对数量巨大的函数,人们只能束手无策。   函数的灵活多样性反映在其副作用上,副作用又是反映在参数的指针和引用传递上。人们利用指针和引用的参数传递,大大地提高了数据传递的效率。但是,人们又在百般提防使用函数的指针参数,因为任何有意无意的越权数据和代码访问都是指针造成的。数据和代码的越权访问是程序缺陷所在。   由于指针和引用,函数的输出数据可以在参数中表示,甚至可以将全局数据用于函数的输入/输出(?CH7.3.2)。   通过规范的函数设计,可以避免许多函数的缺陷。函数的黑盒性是程序设计规范化的基点,如何使用函数黑盒概念设计模块和过程结构,如何利用函数的副作用提高程序的性能,又如何认识到函数副作用的隐蔽性而丰富程序调试的经验,是高级程序员与初级程序员的分水岭。   C++中函数机制是靠栈结构支撑的,不同的编译器处理函数的手法大同小异,栈机制使得函数的执行代码与函数的数据区域相分离,使得函数递归调用成为可能。然而,递归调用全套复制了函数的局部数据,因而对于深度的递归可能在时间和空间上陷入困境。   函数指针的使用有点别扭,使用它需要经验,初学者少有涉及。然而函数指针开拓了程序运行所覆盖的计算机系统空间,它可以让程序优美灵活,也可以让程序充满邪恶。在设计上它是一柄双刃剑。   任何一种程序设计方法,最重要的问题都是程序组织的问题。基于过程的程序设计,也必然涉及程序的组织。推出高级语言,在于非专业程序员要求介入的呼声,在于软件大生产,在于程序员的分工。所以,除了用函数去堆积一个程序外,还需要程序员互相之间的协调,当不在同一台计算机上的多个程序员为同一个工程而忙碌的时候,名称冲突,共享名称使用便成了需要解决的问题。对于包含多个函数的文件的组织问题也需要一并解决。在第7章将介绍函数堆积所构成的结构,即程序的组织问题。   main函数是操作系统调用的函数,因而它是唯一不能由其他函数调用的函数,它的返回类型一定是int型,否则就不是标准的。但是main函数中的返回语句可以网开一面地免去,这也是它的特权。main函数所带的参数规定了操作系统对它数据传递的形式,借此参数,程序可以处理运行命令中的附加信息。重定向是操作系统启动命令的功能之一。   函数可以重载,即重名,但必须有不同的参数类型、个数和顺序。重载函数带来程序设计概念上的亲和性,使得函数名不必处处不同。函数参数可以默认,其效果类似于函数重载,但本质上是两回事。函数重载和默认参数在使用中存在一些差别。 练习5(Exercises 5) 1. 已知函数poly是用递归方法计算x的n阶勒让德多项式的值。数学函数如下:    1 n=0             polyn(x)=x n=1             ((2n–1)*x*polyn–1(x)–(n–1)*polyn–2(x))/n n>1               请写出C++函数,其参数为x和n。并求当x为1.2,n为8时,其poly函数的值。   2.使用函数声明、调用和定义的三部曲,将下列程序用三个函数拆开,并求出运行结果。    //==================================== //e0502.cpp //calcu students grade //==================================== #include using namespace std; //---------------------------------------- const int n = 5; const int n = 4; int a[n][m]; //----------------------------------------- int main(){ //input for(int i=0; i>a[i][j]; //total for(int i=0; i