第8章 函 数 一、函数与调用约定 二、函数的总体概念
一、函数与调用约定 将经常使用的功能组合成一个有机的整体,程序就划分 为功能相对独立的模块。这些独立的模块对应着程序的函 数。 函数是代码最重要的重用机制。函数的来源分为两种: 一、程序员编写的函数; 二、系统提供的标准库函数。 系统的库函数是预先编好的可供程序员调用的函数。调 用时需要将库函数的原型通过头文件的方式包含在主控函数 的源文件中。 例如: 通过#include<math.h>,就可以使用math.h中的数学 函数如sin,cos等。
调用约定是为实现函数调用而建立的一种协议。函数定 义之后,可以在别的地方对它进行调用。在定义时用形式参 数(简称形参),调用时则替换成实际参数(简称实参)。 首先,参数的传递是指入口形参输入传递,它是一个 形实替换过程。 如果一个程序向另一个程序发送参效的规则和后者接收 参数的规则不符合,那么程序就可能因为出错而导致系统崩 溃。 其次,参数的传递也包括函数返回的数据传递。
1. 传值调用 (call by value) 在各种高级语言中比较流行的参数的传递方式主要为如 下两种: 传值是一种最简单的参数传递方法,传值首先指输入传 值,它把实参的值单方向地传递给相应的形参。对于变量、 指针等实参,被调用段无法改变实参的值,传值是一种最安 全的参数传递方法。 传值包括形参的输入传值与数据的返回传值。例如: int ifi (int x) {return x+1;} //int型的数值入口和int型的数值返回 int* pfp (int* p) {return p+2;} // int*型的数值入口和int*型的指针数值返回
输入传值过程把实参值存放在一个被调用段可以取得的 地方即形参中,每一个入口形参位于被调函数新开辟的堆栈 空间而非原来的变量中。 进入被调用段后,首先在临时的堆栈空间取出实参值, 然后象对待一个局部变量一样对形参进行处理。 在被调函数中,一切操作都针对此局部的独立的形参单 元进行。如果实参不是变量的地址,则被调用段是无法改变 实参对应的变量值的,即对原实参对应的变量无影响。 实参为指针的情况,传值也是传递指针所具有的数值不 是指针的地址,即传递另一个变量的地址给被调程序,作为 被调程序指针形参的初始值,在被调用段可以改变指针指向 的存储单元的值,不能改变原实参对应的指针的值。传值调 用传递一个不含实参地址属性信息的右值。
传值调用的特点是先计算出表达式的值,输入的时候对 于大的对象或结构变量须把其一系列具体的数据值放置在刚 开辟的堆栈空间中,堆栈空间的开销趋大。 传值返回的时候根据返回数据类型的大小系统将结果值 或通过EAX等寄存器或通过临时建立的存储单元返回给主控 程序。 C语言函数的参数传递方式只有一种就是数值传递。
2. 引用调用(call by reference) 传递。指针的传值与变量的引用传递都是转送另一个变量的 地址但对应两种不同的调用约定。 引用调用包括变量和指针以及函数指针等的引用传递, 包括引用输入与引用返回。例如: int& rif (int& x) {return x+=1;} //int&变量的引用入口和引用返回 int*& rpf (int*& p) {return p+=2;} //int*&指针的引用入口和引用返回
引用输入传递也是用得最多的参数传递方式,它是把实 际参数的地址传递给相应的形式参数。引用调用传递一个含 实参地址属性信息的左值。其实现过程下: 在被调函数中,每一个形参都对应了一个形参单元,这 个单元用来存放相应实参的地址。 如果实参是变量则直接传递它的地址;如果实参是常数 或表达式,则应该首先计算出它的值并放入一个临时单元, 再把这个临时单元的地址传给函数。 当进入被调用函数后,对应的形参单元中存放的是实参 的固定地址,在处理数据时,针对这些形参指向的实参地址 进行访问,但程序段中采用的是变量名的语法。在被调函数 返回时,这些形参单元所对应的实际单元就直接得到结果值.
如果实参的类型不同于形参的类型,不同版本编译器做 出的反应是不同的: 或者建立临时变量,将临时变量的地址作为下一步计算 的依据; 或者禁止引用形参和实参的类型不一致(vc6.0属此情 况)。引用形参关键之点是被调函数直接操作实参代表的内 存空间。 引用调用的核心在于: 变量具有双重属性,变量的地址属性和变量的值属性, 被调函数拥有实参的双重属性; 传址就是通过变量的地址属性,快捷地存取变量的值, 在函数体中,间接寻址的地址未变,而其地址对应的内容可 发生变化。
引用调用在变量输入的时候不论是小的字符变量或是大 的对象只将实参变量的地址压入堆栈。 传址返回的时候也直接返回相关变量的地址属性,不需 要额外建立临时对象或临时变量。 C++作为C的超集,继承了C语言根深蒂固的参数传值 调用方式,又借鉴FORTRAN 等高级语言中高效的引用调 用,形成两种调用并存的局面。 C++语言是典型的混合编程语言。
二、函数的总体概念 C/C++中函数是由称之为函数体的可整体运行的若干语 句构成,这些语句对表征数据状态的名称进行预定的运算操 作。 编译器根据函数名、形参类型、形参个数与形参位置来 鉴别函数体代码段入口地址的唯一性,函数调用作为表达式 可返回某种类型的数据。 类名抽象type,T1, T2, T等可以是int, char,long,float 等,也可是结构名、或类类型名等。
1.函数的返回类型 函数的返回类型由函数名前的类型标识符指定。不失一般性以两个形参进行说明。根据函数的调用机制与返回结果可以分为两大种类: a. 函数操作结果没有返回值。例如 void vf (T1,T2*){ return ;} [例] void vf (int a,int* p) { a+=3; p++;} 用关键字void前置加以声明的函数称为void型函数。 void型函数只独立调用,一般不参入表达式的嵌套运算,除 非出现在三目操作数表达式的后两个操作数中以及逗号运算 符分隔的操作数中。
b. 函数操作的结果具有确定的返回值,这样的函数调 用是表达式,可参入表达式的嵌套运算,也可单独调用。 主要可分为两种形式: (1)返回一个数值类型的数据。例如: type funct (T1 v, T2* p) ; type* pfunct (T1 x , T2* q) ; [例] long min(short s,long* p) {return s<*p?s:*p; } long* pan(int n , long* p){ return p+n;};
这种形式的函数操作的结果常见的有两种: (一)、type型的传值返回; (二)、指针的传值; 返回即函数返回的结果为type*型的指针值。指针的值 最终必然用于操作内存的数据,因此该指针监控的内存空间 的生存期对于主控程序应是有效地可访问的。 返回算术或指针类型数值的函数调用为右值表达式。 例如: 函数调用min (s,p)是long型的右值,pan(n,p)是long* 型的右值地址,但间接访问*pan (n,p)是long型的左值。
(2)返回一个引用类型的数据。例如: type& rf (T1&, T2& ) ; type*& rf (T1&, T2* &); [例] long& rmax (long& n,long& m) { return n>m?n:m; } long*& rpan (int& n,long*& p) { return p+=n; } 返回type&类型的函数为返回变量的引用的函数。返回 type*&类型的函数为返回指针的引用的函数。返回引用的函 数调用为左值表达式。 例如:函数调用rmax(n,m)是long型的左值变量,函数 调用rpan ( n, p)是long*型的左值指针。引用概念是C++所 独有的。 函数不能返回函数和数组,但可以返回指向数组或函数的 指针。
2.函数的定义 a.函数数值传递的定义 type funct (T1 x, T2* y) /*标题头*/ { /*x是变量的数值形参,y是指针数值形参*/ 语句序列; return expre; /*expre是可转换为type型的表达式*/ } /*最外的一对花括号界定函数体*/ 例: long min (long x,short* y) { if (x<*y) return x; else return *y; }
b.函数引用传递的定义 type & rf (T & v, T* & p) { /*v是变量的引用形参,p是指针引用形参*/ 语句序列; return Lvalue; /*Lvalue 为type型的左值*/ } 例: int& rmin (int& v, int*& p) { if (v<*p) return v; else return *p;
由于传值的输入不改变相关实参变量的值,如果要保持 原先函数的这一特性,在引用形参前进一步加上修饰词 const,得到: const T& cf(const T& v, T*& p) { 语句序列; return (T型的变量); } 例: const int& cmin (const int& v, int*& p) { if (v<*p) return v; else return *p;
形参表中的const引用形参v是局限于函数体的不变量, 这种出现在形参位置的不变量虚实结合时可以获得不同的实 参变量,在函数体中只作为右值。 函数的返回类型前加const关键字界定,此时限制返回 引用的函数调用不作为左值但保留数据传递的效率。 函数名代表了函数的入口地址。圆括号中()可以无任何 参数,称为无参函数,此时圆括号中可放置关键字void。在 这种情况下对函数的调用称为无参调用。 圆括号()包括的参数称为形式参数,代表函数与调用 段的数据接口。T1,T2, T,type 是类型名,声明数据的类型, v,p, x,y是形参名。
函数名、类型名和形参名遵循标识符的命名规定。参数 之间用逗号分隔,函数名前的类型就是函数的返回类型。 函数不能嵌套定义即在函数体中定义另外一个函数实 现。 函数定义意味形参未曾进行实参化,函数调用则对应虚 实结合过程。 函数体内无论多少条语句,花括号是不可省的。用花括 号括起来的语句序列组成了函数体。
语句序列可以是0条、1条或多条语句。 当函数体是0条语句时,称该函数为空函数。空函数作 为一种什么都不执行的函数也是有意义的。 例如: 下面的NoOperate()就是一个空函数。 void NoOperate (void) { } 函数可以略去形参名仅带类型参数,通常表示预定的接口 准备。例如: void PreOperate (long/*cx*/,float/*dx*/) { }
3.函数原型 函数的类型首先指函数执行后返回值的类型,其次包括 函数的入口类型,两者一起确定函数的类型属性。函数原型 (function prototype)反映函数的类型属性,函数原型称为 函数说明。C++中所有的名称在索引点之前必须有效说明。 函数定义在先调用在后,调用前可以不必说明;函数定 义在后,调用在先,调用前必须说明。一般将函数原型的说 明放在程序全局范围的开始部分。 函数原型表明入口参数的类型、位置、个数和返回类 型。函数原型的说明方法如下: type funct (T1 v, T2* p); 类型 函数名(类型1 形参1,类型2* 形参2);
函数原型中的形参名可以和标题头相应的形参名不同。 例如: long& rmax (long& n,long& m); long& rmax (long& x,long& y); 原型中形参名是可有可无的,但函数原型中形参的位 置、类型、个数与函数名一起构成函数原型的关键因素。写 成略去形参名的形式: long& rmax (long&,long&); 或一般地: type funct (T1 , T2* ); 类型 函数名 (类型1,类型2* );
从函数原型可知funct函数第一个形参的数据类型为 T1,第二个形参为T2*型的指针数值形参,返回type型的数 据。 函数原型与函数定义的标题头十分类似:将函数定义的 标题头进行复制就得到函数原型的说明,但记住在末尾加一 个分号";"。 编译器根据函数原型或标题头来识别不同的函数实现, 函数实现部分中的语句序列对于函数的区别并不重要。
下面是两个重要函数printf和scanf的原型,原型是从系 统stdio.h头文件拷贝过来的,进行了适当简化。三点省略 号...表示可变参量列表,表示实参的个数是动态可变动的。 int printf (const char *format, ...); int scanf (const char *, ...); const char *表示只读指针形参,实参常见的是格式控 制字符串常数。例如: printf ("abcd"); printf ("%d,%d",x,y); scanf("%d,%d",&x,&y); 可以改写为: const char * r="abcd"; const char *fmt="%d,%d"; printf (r); printf (fmt,x,y); scanf (fmt,&x,&y);
4.return 语句 函数的返回通过return语句实现。return语句有三种格 式,如下所示: 格式一: return; 格式二: return expre; return (表达式); 格式三: return Lvalue; return 左值表达式; return语句后面的圆括号是可选的,return (expre);等 价于return expre;
关于return语句的使用说明如下: 格式一用于void型无返回值的情况。无返回值的函数须 用void来说明。该函数中可以有return语句,也可以无 return语句。 当一个被调用函数中无return语句时,程序执行到函数 体的最后一条语句时,返回到调用函数,相当于函数体的花 括号有返回的功能。 函数中也可以有多个return,它们大多出现在if语句 中。当使用无表达式的返回语句时,返回程序执行的控制 权。
格式二是用于数值返回的调用函数中,这也是传统的C 语言中的返回方式,return之后表达式的值就是函数调用的 值。最简单的表达式是变量或常数。 格式三用于引用返回的函数调用中,这是C++新增的函 数返回约定。这种约定对应着对引用或相关变量的直接操 作,因此当函数引用返回时返回语句return后不要跟右值表 达式,而只返回左值表达式。表达式expre或Lvalue可以是 返回值的函数调用。
具有return 语句实现过程如下: a. 先计算出表达式expre或Lvalue的值,表达式完成所 有的副作用。 b. 算术表达式expre的类型与函数的类型不相同时,将 表达式的类型自动转换为函数的返回类型,这种转换是强制 性的,可以出现不保值的情况。 返回引用时的左值一般应与返回类型一致。指针表达式 也应与函数的返回类型一致。 c. 将程序执行的控制权由被调函数转向主控函数, 执 行主控函数后面的语句。
图 函数调用和返回过程 void func1() { func2(); int func2() 语句; { 语句; ::: return; } int func2() { 语句; ::: return 1; } void main(void) { 语句; func1(); 语句1; ... funck(); 语句k; funcn(); 语句n; } void funck() { func(); 语句; ::: return; } char func() { 语句; ::: return '1'; } 图 函数调用和返回过程
函数体中的变量为局部变量,非静态的局部变量存放在 堆栈空间。 堆栈空间是动态变化的,函数是分层分层调用的。主控 函数与被调函数各自的局部变量都位于堆栈空间中,被调函 数的局部变量位于堆栈空间变化快的部分,主控函数的局部 变量位于堆栈空间变化慢的部分。不要返回变化快的局部变 量的堆栈地址给上层的主控函数。 即上面的 Lvalue 应是形参表中的引用或全局变量或全 局数组元素或静态变量。
5.函数的使用 函数定义之后就可以使用,使用函数也称为函数调用。 函数名加上圆括号包括的不带类名的实参序列构成函数 调用。 函数调用分三个主要步骤: a. 虚实结合 b. 执行函数体中的代码序列 c. 函数返回 函数调用对应形参初始化。实参对形参初始化是按其位 置对应进行,即第一个实参的值赋给第一个形参,第二个实 参的值赋给第二个形参,依此类推。
实参表达式在虚实结合之前已进行求值计算,编译器习 惯上从右到左的次序对实参表达式进行求值计算。 但可以从左边对应的实参开始求值一直往右,这种实参 求值次序的不同可能引起函数调用的一些副作用,因此编程 时保证实参表达式的值独立于编译器左右求值的不确定性。 对于算术型数值形参,如果实参类型不同于形参类型则 进行必要的类型转换,转换的方向从实参的类型到形参的类 型。指针形参和引用形参要求严格匹配相应的实参类型。 数值形参重要的性质是其安全地承前启后的作用,在堆 栈获得初始值之后,数值形参本身在函数体中的变化不影响 主控函数,并且它们占有的独立的内存可以充分加以利用。
引用形参对应的实参是变量或左值表达式,执行传址调 用之前系统得到的是实参变量的地址,传递给形参的是左值 实参,隐含地通过左值实参的地址间接操作实参的数值。 函数单独调用构成函数调用语句,有值返回的函数单独 调用时,系统会建立一个临时变量以保存函数的返回值。 函数调用的一般格式为: 函数指针表达式(实参表达式列表) 函数名是最简单的函数指针表达式。
函数调用根据函数的返回类型来确定: (1). 无值返回的函数单独调用。如: vf (v1,v2); swap (&a,&b); swap (a,b); //函数调用语句 (2). 返回右值的函数可作为右值表达式参入各种运算。 如: var=20* funct (v1,v2); int x=min (n,p); long* q=pan (n,p); (3). 返回左值即返回引用的函数可作为左值表达式参入 各种运算。如: rf (v1,v2)+=1; rmax (x,y)-=2; rpan (n,p)--;
返回值的函数调用作为表达式其结果就是return中转送 过来的表达式,这称为函数对于相应表达式的映射。 返回引用的函数Lvalue可以出现的地方函数调用 rf(v1,v2)作为左值可以等价地出现,返回算术或指针数值的 函数funct将expre映射成右值表达式。 在这种映射之前函数可以执行许多有意义的运算。 返回值的函数既具有动作本身又携带数据信息,这一性 质表明函数是算法和数据封装的基石。
请打开“第8章(2).ppt”