1、 面向对象程序设计复习辅导( 三) -函 数 徐孝凯 一个C+语言程序由若干个程序文件和头文件所组成, 每个头文件中一般带有用户类型的定义、 符号常量的定义、 函数的声明等内容, 每个程序文件由若干个函数定义所组成, 其中必有一个而且只有一个程序文件中包含有主函数main, 称此程序文件为主程序文件。函数是C+程序中的基本功能模块和执行单元, 这一章专门讨论函数的定义和调用, 变量的作用域和生存期等内容。 一、 函数的定义 ( 一) 定义格式 () 为系统或用户已定义的一种数据类型, 它是函数执行过程中经过return语句要求返回的值的类型, 又称为该函数的类型。当一个函数不需要经过retur
2、n语句返回一个值时, 称为无返回值函数或无类型函数, 此时需要使用保留字void作为类型名。当类型名为int时, 能够省略不写, 但为了清楚起见, 还是写明为好。 是用户为函数所起的名字, 它是一个标识符, 应符合C+标识符的一般命名规则, 用户经过使用这个函数名和实参表能够调用该函数。 又称形式参数表, 它包含有任意多个( 含0个, 即没有) 参数说明项, 当多于一个时其前后两个参数说明项之间必须用逗号分开。每个参数说明项由一种已定义的数据类型和一个变量标识符组成, 该变量标识符成为该函数的形式参数, 简称形参, 形参前面给出的数据类型称为该形参的类型。一个函数定义中的能够被省略, 表明该函
3、数为无参函数, 若用void取代, 则也表明是无参函数, 若不为空, 同时又不是保留字void, 则称为带参函数。 是一条复合语句, 它以左花括号开始, 到右花括号结束, 中间为一条或若干条C+语句。 在一个函数的参数表中, 每个参数能够为任一种数据类型, 包括普通类型、 指针类型、 数组类型、 引用类型等, 一个函数的返回值能够是除数组类型之外的任何类型, 包括普通类型、 指针类型和引用类型等。另外, 当不需要返回值时, 应把函数定义为void类型。 ( 二) 定义格式举例 (1) void f1() . (2) void f2(int x) . (3) int f3(int x,int*
4、p) . (4) char* f4(char a). (5) int f5(int& x,double d) . (6) int& f6(int b10, int n) . (7) void f7(float cN, int m, float& max) . (8) bool f8(ElemType*& bt, ElemType& item) . 在第一条函数定义中, 函数名为f1, 函数类型为void, 参数表为空, 此函数是一个无参无类型函数。若在f1后面的圆括号内写入保留字void, 也表示为无参函数。 在第二条函数定义中, 仅带有一个类型为int的形参变量x, 该函数没有返回值。 在第
5、三条函数定义中, 函数名为f3, 函数类型为int, 函数参数为x和p, 其中x为int型普通参数, p为int*型指针参数。 在第四条函数定义中, 函数名为f4, 函数类型为char*, 即字符指针类型, 参数表中包含一个一维字符数组参数。注意: 在定义任何类型的一维数组参数时, 不需要给出维的尺寸, 当然给出也是允许的, 但没有任何意义。 在第五条函数定义中, 函数名为f5, 返回类型为int, 该函数带有两个形参, 一个为 整型引用变量x, 另一个为双精度变量d。 在第六条函数定义中, 函数名为f6, 函数类型为int&, 即整型引用, 该函数带有两个形参, 一个是整型数组b, 另一个是
6、整型变量n。在这里定义形参数组b所给出的维的尺寸10能够被省略。 在第七条函数定义中, 函数名为f7, 无函数类型, 参数表中包含三个参数, 一个为二维单精度型数组c, 第二个为整型变量m, 第三个为单精度引用变量max。注意: 当定义一个二维数组参数时, 第二维的尺寸必须给出, 而且必须是一个常量表示式, 第一维尺寸可给出也可不给出, 其作用相同。 在第八条函数定义中, 函数名为f8, 返回类型为bool, 即逻辑类型, 该函数带有两个参数, 一个为形参bt, 它为ElemType的指针引用类型, 另一个为形参item, 它是ElemType的引用类型, 其中ElemType为一种用户定义的
7、类型或是经过typedef语句定义的一个类型的别名。 ( 三) 有关函数定义的几点说明 1. 函数原型语句 在一个函数定义中, 函数体之前的所有部分称为函数头, 它给出了该函数的返回类型、 每个参数的次序和类型等函数原型信息, 因此当没有专门给出函数原型说明语句时, 系统就从函数头中获取函数原型信息。 一个函数必须先定义或声明而后才能被调用, 否则编译程序无法判断该调用的正确性。一个函数的声明是经过使用一条函数原型语句实现的, 当然使用多条相同的原型语句声明同一个函数虽然多余但也是允许的, 编译时不会出现错误。 在一个完整的程序中, 函数的定义和函数的调用能够在同一个程序文件中, 也能够处在不
8、同的程序文件中, 但必须确保函数原型语句与函数调用表示式出现在同一个文件中, 而且函数原型语句出现在前, 函数的调用出现在后。 一般把一个程序中用户定义的所有函数的原型语句组织在一起, 构成一个头文件, 让该程序中所含的每个程序文件的开始( 即所有函数定义之前) 包含这个头文件( 经过#include命令实现) , 这样不论每个函数的定义在哪里出现, 都能够确保函数先声明后使用( 即调用) 这一原则的实现。 一个函数的原型语句就是其函数头的一个拷贝, 当然要在最后加上语句接上结束符分号。函数原型语句与函数头也有细微的差别, 在函数原型语句中, 其参数表中的每个参数允许只保留参数类型, 而省略参
9、数名, 而且若使用参数名也允许与函数头中对应的参数名不同。 2. 常量形参 在定义一个函数时, 若只允许函数体访问一个形参的值, 不允许修改它的值, 则应把该形参说明为常量, 这只要在形参说明的前面加上const保留字进行修饰即可。如: void f9(const int& x, const char& y); void f10(const char* p, char key); 在函数f9的函数体中只允许使用x和y的值, 不允许修改它们的值。在函数f10的函数体中只允许使用p所指向的字符对象或字符数组对象的值, 不允许修改它们的值, 但在函数体中既允许使用也允许修改形参key的值。 3. 缺
10、省参数 在一个函数定义中, 可根据需要对参数表末尾的一个或连续若干个参数给出缺省值, 当调用这个函数时, 若实参表中没有给出对应的实参, 则形参将采用这个缺省值。如: void f11(int x, int y=0) . int f12(int a, char op=+, int k=10) . 函数f11的定义带有两个参数, 分别为整型变量x和y, 而且y带有缺省值0, 若调用该函数的表示式为f11(a,b), 将把a的值赋给x, 把b的值赋给y, 接着执行函数体; 若调用该函数的表示式为f11(a+b), 则也是正确的调用格式, 它将把a+b的值赋给x, 因y没有对应的实参, 将采用缺省值
11、0, 参数传送后接着执行函数体。 函数f12的定义带有三个参数, 其中后两个带有缺省值, 因此调用它的函数格式有三种, 一种只带一个实参, 用于向形参a传送数据, 后两个形参采用缺省值, 第二种带有两个实参, 用于分别向形参a和op传送数据, 第三个形参采用缺省值, 第三种带有三个实参, 分别用于传送给三个形参。 若一个函数带有专门的函数原型语句, 则形参的缺省值只能在该函数原型语句中给出, 不允许在函数头中给出。如对于上述的f11和f12函数, 其对应的函数原型语句分别为: void f11(int x, int y=0); int f12(int a, char op=+, int k=1
12、0); 函数定义应分别改写为: void f11(int x, int y) . int f12(int a, char op, int k) . 4. 数组参数 在函数定义中的每个数组参数实际上是指向元素类型的指针参数。对于一维数组参数说明: 它与下面的指针参数说明完全等价: * 其中就是数组参数说明中的。如对于f12函数定义中的数组参数说明int a, 等价于指针参数说明int* a。也就是说, 数组参数说明中的数组名a是一个类型为int*的形参。注意: 在变量定义语句中定义的数组, 其数组名代表的是一个数组, 它的值是指向第一个元素的指针常量, 这与数组形参的含义有区别。 对于二维数组参
13、数说明: 它与下面的指针参数说明完全等价: (*) 如对于f7函数定义中的二维数组参数说明float cN, 等价于指针参数说明float(*c)N。 5. 函数类型 当调用一个函数时就执行一遍循环体, 对于类型为非void的函数, 函数体中至少必须带有一条return语句, 而且每条return语句必须带有一个表示式, 当执行到任一条return语句时, 将计算出它的表示式的值, 结束整个函数的调用过程, 把这个值作为所求的函数值带回到调用位置, 参与相应的运算; 对于类型为void的函数, 它不需要返回任何函数值, 因此在函数体中既能够使用return语句, 也能够不使用, 对于使用的每条
14、return语句不允许也不需要带有表示式, 当执行到任一条return语句时, 或执行到函数体最后结束位置时, 将结束函数的调用过程, 返回到调用位置向下继续执行。 6. 内联函数 当在一个函数的定义或声明前加上关键字inline则就把该函数声明为内联函数。计算机在执行一般函数的调用时, 无论该函数多么简单或复杂, 都要经过参数传递、 执行函数体和返回等操作。若把一个函数声明为内联函数后, 在程序编译阶段系统就有可能把所有调用该函数的地方都直接替换为该函数的执行代码, 由此省去函数调用时的参数传递和返回操作, 从而加快整个程序的执行速度。一般可把一些相对简单的函数声明为内联函数, 对于较复杂的
15、函数则不应声明为内联函数。从用户的角度看, 调用内联函数和一般函数没有任何区别。下面就是一个内联函数定义的例子, 它返回形参值的立方。 inline int cube(int n) return n*n*n; 二、 函数的调用 ( 一) 调用格式 调用一个已定义或声明的函数需要给出相应的函数调用表示式, 其格式为: () 若调用的是一个无参函数, 或全部形参为可选的函数, 则被省略, 此时实参表为空。 为一个或若干个用逗号分开的表示式, 表示式的个数应至少等于不带缺省值的形参的个数, 应不大于所有形参的个数, 中每个表示式称为一个实参, 每个实参的类型必须与相应的形参类型相同或兼容( 即能够被
16、自动转换为形参的类型, 如整型与字符型就是兼容类型) 。每个实参是一个表示式, 包括是一个常量、 一个变量、 一个函数调用表示式, 或一个带运算符的一般表示式。如: (1) g1(25) /实参是一个整数 (2) g2(x) /实参是一个变量 (3) g3(a,2*b+3) /第一个为变量, 第二个运算表示式 (4) g4(sin(x),) /第一个为函数调用表示式, 第二个为字符常量 (5) g5(&d,*p,x/y-1) /分别为取地址运算、 间接访问和一般运算表示式 任一个函数调用表示式都能够单独作为一条表示式语句使用, 但当该函数调用带有返回值时, 这个值被自动丢失。对于具有返回值的函
17、数, 调用它的函数表示式一般是作为一个数据项使用, 用返回值参与相应的运算, 如把它赋值给一个变量, 把它输出到屏幕上显示出来等。如: (1) f1(); /作为单独的语句, 若有返回值则被丢失 (2) y=f3(x,a); /返回值被赋给y保存 (3) coutf6(c,10)endl; /返回值被输出到屏幕上 (4) f2(f5(x1,d1)+1); /f2调用作为单独的语句, /f5调用是f2实参表示式中的一个数据项 (5) f6(b,5)=3*w-2; /f6函数调用的返回值当作一个左值 (6) if(f8(ct,x) cout”true”endl; / f6函数调用作为一个判断条件,
18、 /若返回值不为0则执行后面的输出语句, 否则不执行任何操作 ( 二) 调用过程 当调用一个函数时, 整个调用过程分为三步进行, 第一步是参数传递, 第二步是函数体执行, 第三步是返回, 即返回到函数调用表示式的位置。 参数传递称为实虚结合, 即实参向形参传递信息, 使形参具有确切地含义( 即具有对应的存储空间和初值) 。这种传递又分为两种不同情况, 一种是向非引用参数传递, 另一种是向引用参数传递。 形参表中的非引用参数包括普通类型的参数、 指针类型的参数和数组类型的参数三种。实际上能够把数组类型的参数归为指针类型的参数。 当形参为非引用参数时, 实虚结合的过程为: 首先计算出实参表示式的值
19、, 接着给对应的形参变量分配一个存储空间, 该空间的大小等于该形参类型的长度, 然后把已求出的实参表示式的值存入到为形参变量分配的存储空间中, 成为形参变量的初值。这种传递是把实参表示式的值传送给对应的形参变量, 称这种传递方式为”按值传递”。 假定有下面的函数原型: (1) void h1(int x, int y); (2) bool h2(char*p); (3) void h3(int a, int n); (4) char* h4(char bN, int m); 若采用如下的函数调用: (1) h1(a,25); /假定a为int型(2) bool bb=h2(sp); /假定sp
20、为char*型(3) h3(b,10); /假定b为int*型(4) char* s=h4(c,n+1); /假定c为int(*)N型, n为int型 当执行第一条语句中的h1(a,25)调用时, 把第一个实参a的值传送给对应形参x的存储空间, 成为x的初值, 把常数25传送给形参y的存储空间, 成为y的初值。 当执行第二条语句中的h2(sp)调用时, 将把sp的值, 即一个字符对象的存储地址传送给对应的指针形参p的存储空间中, 使p指向的对象就是实参sp所指向的对象, 即*p和*sp指的是同一个对象, 若在函数体中对*p进行了修改, 则待调用结束返回后经过访问*sp就得到了这个修改。 当执行
21、第三条语句中的h3(b,10)调用时, 将把b的值( 一般为元素类型为 int的一维数组的首地址) 传送给对应数组变量( 实际为指针变量) a的存储空间中, 使得形参a指向实参b所指向的数组空间, 因此, 在函数体中对数组a的存取元素的操作就是对实参数组b的操作。也就是说, 采用数组传送能够在函数体中使用形参数组访问对应的实参数组。 当执行第四条语句中的h4(c,n+1)调用时, 将把c的值( 一般为与形参b具有相同元素类型和列数的二维数组的首地址) 传送给对应二维数组参数( 实际为指针变量) a的存储空间中, 使得形参b指向实参c所指向的二维数组空间, 在函数体中对数组b的存取元素的操作就是
22、对实参数组c的操作; 该函数调用还要把第二个实参表示式n+1的值传送给形参m中, 在函数体中对m的操作与相应的实参无关。 在函数定义的形参表中说明一个数组参数时, 一般还需要说明一个整型参数, 用它来接收由实参传送来的数组的长度, 这样才能够使函数知道待处理元素的个数。 当形参为引用参数时, 对应的实参一般是一个变量, 实虚结合的过程为: 把实参变量的地址传送给引用形参, 成为引用形参的地址, 也就是说使得引用形参是实参变量的一个引用( 别名) , 引用形参所占用的存储空间就是实参变量所占用的存储空间。因此, 在函数体中对引用形参的操作实际上就是对被引用的实参变量的操作。这种向引用参数传递信息
23、的方式称为引用传送或按址传送。 引用传送的好处是不需要为形参分配新的存储空间, 从而节省存储, 另外能够使对形参的操作反映到实参上, 函数被调用结束返回后, 能够从实参中得到函数对它的处理结果。有时, 既为了使形参共享实参的存储空间, 又不希望经过形参改变实参的值, 则应当把该形参说明为常量引用, 如: void f13(const int& A, const Node*& B, char C); 在该函数执行时, 只能读取引用形参A和B的值, 不能够修改它们的值。因为它们是对应实参的别名, 因此, 也能够说, 只允许该函数使用A和B对应实参的值, 不允许进行修改, 从而杜绝了对实参进行的有意
24、或无意的破坏。 进行函数调用除了要把实参传递给形参外, 系统还将自动把函数调用表示式执行后的位置( 称为返回地址) 传递给被调用的函数, 使之保存起来, 当函数执行结束后, 将按照所保存的返回地址返回到原来位置, 继续向下执行。 函数调用的第二步是执行函数体, 实际上就是执行函数头后面的一条复合语句, 它将按照从上向下、 从左向右的次序执行函数体中的每条语句, 当碰到return语句时就结束返回。对于无类型函数, 当执行到函数体最后的右花括号时, 与执行一条不带表示式的return语句相同, 也将结束返回。 函数调用的第三步是返回, 这实际上是执行一条return语句的过程。当return语句
25、不带有表示式时, 其执行过程为: 按函数中所保存的返回地址返回到调用函数表示式的位置接着向下执行。当return语句带有表示式时, 又分为两种情况, 一种是函数类型为非引用类型, 则计算出return表示式的值, 并把它保存起来, 以便返回后访问它参与相应的运算; 另一种情况是函数的类型为引用类型, 则return中的表示式必须是一个左值, 而且不能是本函数中的局部变量( 关于局部变量的概念留在下一节讨论) , 执行return语句时就返回这个左值, 也能够说函数的返回值是该左值的一个引用。因此, 返回为引用的函数调用表示式既可作为右值又可作为左值使用, 但非引用类型的函数表示式只能作为右值使
26、用。例如: int& f14(int a, int n) int k=0; for(int i=1;iak) k=i; return ak; 该函数的功能是从一维整型数组an中求出具有最大值的元素并引用返回。当调用该函数时, 其函数表示式既能够作为右值, 从而取出ak的值, 又能够作为左值, 从而向ak赋予新值。如: #include int& f14(int a, int n) int k=0; for(int i=1;iak) k=i; return ak; void main() int b8=25,37,18,69,54,73,62,31; coutf14(b,8)endl; f14(
27、b,5)=86; for(int i=0;i8;i+) coutbi ; coutendl; 该程序的运行结果如下, 请读者自行分析。 73 25 37 18 86 54 73 62 31 一般把函数定义为引用的情况较少出现, 而定义为非引用( 即普通类型和指针类型) 的情况则常见。 ( 三) 函数调用举例 程序1: #include int xk1(int n); void main() coutm; int sum=xk1(m)+xk1(2*m+1); coutsumendl; int xk1(int n) int i,s=0; for(i=1;i=n;i+) s+=i; return s
28、; 该程序包含一个主函数和一个xk1函数, 在程序开始给出了一条xk1函数的原型语句, 使得xk1函数无论在什么地方定义, 在此程序文件中的所有函数都能够合法地调用它。注意: 主函数不需要使用相应的函数原型语句加以声明, 因为C+规定不允许任何函数调用它, 它只由操作系统调用并返回操作系统。 函数xk1的功能是求出自然数1至n之和, 这个和就是s的最后值, 由return语句把它返回。在主函数中首先为m输入一个自然数, 接着用m去调用xk1函数返回1至m之间的所有自然数之和, 再用2*m+1去调用xk1函数返回1至2*m+1之间的所有自然数之和, 把这两个和加起来赋给变量sum, 最后输出su
29、m的值。 假定从键盘上为m输入的正整数为5, 则进行xk1(m)调用时把m的值5传送给n, 接着执行函数体后返回s的值为15, 进行xk1(2*m+1)调用时把2*m+1的值11传送给n, 接着执行函数体后返回s的值为66, 它们的和81被作为初值赋给sum, 最后输出的sum值为81。 程序2: #include void xk2(int& a, int b); void main() int x=12,y=18; coutx=x y=yendl; xk2(x,y); coutx=x y=yendl; void xk2(int& a, int b) couta=a b=bendl; a=a+
30、b; b=a+b; couta=a b=bendl; 该程序包含一个主函数和一个xk2函数, xk2函数使用了两个形参, 一个是整型引用变量a, 另一个是整型变量b。在主函数中使用xk1(x,y)调用时, 将使形参a成为实参x的别名, 在函数体中对a的访问就是对主函数中x的访问, 此调用同时把y的值传送给形参b, 在函数体中对形参b的操作是与对应的实参y无关的, 因为它们使用各自的存储空间。该程序的运行结果为: x=12 y=18 a=12 b=18 a=30 b=48 x=30 y=18 程序3: #include void xk3(int* a, int* b); void xk4(int
31、& a, int& b); void main() int x=5,y=10; coutx=x y=yendl; xk3(&x, &y); coutx=x y=yendl; xk4(x, y); coutx=x y=yendl; void xk3(int* a, int* b) int c=*a; *a=*b; *b=c; void xk4(int& a, int& b) int c=a; a=b; b=c; 该程序中的xk3函数用于交换a和b分别指向的两个对象的值, 主函数使用xk3(&x, &y)调用时, 分别把x和y的地址赋给形参a和b, 因此实际交换的是主函数中x和y的值; xk4函数
32、用于直接交换a和b的值, 由于a和b都是引用参数, 因此在主函数使用xk4(x,y)调用时, 执行xk4函数实际交换的是相应实参变量x和y的值。 此程序的运行结果为: x=5 y=10 x=10 y=5 x=5 y=10 上述的xk3和xk4具有完全相同的功能, 但由于在xk3中使用的是指针参数, 传送给它的实参也必须是对象的地址, 在函数体中访问指针所指向的对象必须进行间接访问运算, 因此, 定义和调用xk3不如定义和调用xk4直观和简便。 程序4: #include const int N=8; int xk5(int a, int n); void main() int bN=1,7,2
33、,6,4,5,3,-2; int m1=xk5(b,8); int m2=xk5(&b2,5); int m3=xk5(b+3,3); coutm1 m2 m3endl; int xk5(int a, int n) int i,f=1; for(i=0;in;i+) f*=ai; /或写成f*=*a+; return f; 该函数包含一个主函数和一个xk5函数, xk5函数的功能是求出一维整型数组an中所有元素之积并返回。在主函数中第一次调用xk5函数时, 把数组b的首地址传送给a, 把数组b的长度8传送给n, 执行函数体对数组a的操作实际上就是对主函数中数组b的操作, 因为它们同时指向数组b
34、的存储空间; 第二次调用xk5函数是把数组b中b2元素的地址传送给a, 把整数5传送给n, 执行函数体对数组an的操作实际上是对数组b中b2至b6之间元素的操作; 第三次调用xk5函数是把数组b中b3元素的地址传送给a, 把整数3传送给n, 执行函数体对数组an的操作实际上是对数组b中b3至b5之间元素的操作。该程序的运行结果为: -10080 720 120 程序5: #include char* xk6(char* sp, char* dp); void main() char a15=abcadecaxybcw; char b15; char* c1=xk6(a,b); coutc1 a
35、 bendl; char* c2=xk6(a+4,b); coutc1 a bendl; char* xk6(char* sp, char* dp) if(*sp=0) *dp=0; return dp; int i=0,j; for(char* p=sp; *p; p+) /扫描sp所指字符串中的每个字符位置 for(j=0;j=i) dpi+=*p; /若dp数组的前i个元素均不等于*p, 则把*p写入dpi元素中 dpi=0; /写入字符串结束符 return dp; xk6函数的功能是把sp所指向的字符串, 去掉重复字符后拷贝到dp所指向的字符数组中, 并返回dp指针。在主函数中第一次
36、调用xk6函数时, 分别以a和b作为实参, 第二次调用时分别以a+4( 即a4的地址) 和b作为实参。该程序运行后的输出结果为: abcdexyw abcadecaxybcw abcdexyw decaxybw abcadecaxybcw decaxybw 程序6: #include int* xk7(int*& a1, int* a2); int* xk7(int*& a1, int* a2) coutwhen enter xk7: *a1,*a2=*a1, *a2endl; a1=new int(2*a1+4); a2=new int(2*a2-1); coutwhen leave xk7
37、: *a1,*a2=*a1, *a2endl; return a2; void main() int x=10, y=25; int *xp=&x, *yp=&y; coutbefore call xk7: *xp,*yp=*xp, *ypendl; int* ip=xk7(xp,yp); coutafter call xk7: *xp,*yp=*xp, *ypendl; cout*ip=*ipendl; delete xp; /xp指向的是在执行xk7函数时动态分配的对象*a1 delete ip; /ip指向的是在执行xk7函数时动态分配的对象*a2 在xk7函数的定义中, 把形参a1定义
38、为整型指针的引用, 把a2定义为整型指针, 当在主函数中利用xk7(xp,yp)表示式调用该函数时, a1就成为xp的别名, 访问a1就等于访问主函数中的xp, 而a2同yp具有各自独立的存储空间, a2的初值为yp的值, 在xk7函数中对a2的访问(指直接访问)与yp无关。此程序运行结果为: before call xk7: *xp,*yp=10, 25 when enter xk7: *a1,*a2=10, 25 when leave xk7: *a1,*a2=24, 49 after call xk7: *xp,*yp=24, 25 *ip=49 三、 变量的作用域 在一个C+程序中, 对于每个变量必须遵循先定义后使用的原则。根据变量定义的位置不同将使它具有不同的作用域。一个变量离开了它的作用域, 在定义时为它分配的存储空间就被系统自动回收了, 因此该变量也就不