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