3.3 函数类型和应用方法
3.3.1 函数定义与调用函数是程序内部为完成特定功能而构造的独立的程序块。每个函数都具有独立的功能,明确的界面,形成清晰的模块结构,与程序的其他部分分隔开来。
函数定义的语法结构如下:
store_type data_type name(type1 data1,type data2,…..,typen datan)
{
statements;
}
其中store_type是函数的存储类型,它是可省略的;data_type是函数的返回值类型;name是函数名,用来标识函数;小括号括起来的一组数据类型和参数构成了函数的参数表(形式参数),其中type1~typen是n个参数的变量名;被大括号括起来的一组语句statements是执行相应功能的函数体。C++规定,函数的定义不能嵌套,即一个函数的内部不能定义其他函数,每个函数都是相对独立的程序块。各函数定义的顺序是任意的,不影响程序运行时函数调用的顺序。
如:
inline double abs(double val)
{
return (val<0?-val:val);
}
inline double max(double val1,double val2)
{
return (val1>val2)?val1:val2);
}
一个程序内的函数,即使其作用域的重叠,也可以是同名的,但同名函数的参数表必须不能完全相同。在函数调用时,编译器会根据函数名及参数的类型及数目自动确定调用哪一个函数。函数必须先定义或说明后使用,否则会导致编译错误。在一个程序内,函数必须且只能定义一次,但可以被多次说明。函数说明时只需要包括函数的返回值类型、函数名和参数表,这三项组成了函数原型。例如上面列举的确良个函数的函数原型分别为
double abs(double);
double max(double,double);
调用函数的形式是在函数名后加一对小括号,小括号内包含有逗号分隔开的相应数目的参数(称为实参数)——常量或变量。在函数调用时,编译器将各个实参数有值分另代入形式参数中(称为参数传递),并利用这些值进行函数体所规定的计算或其他操作,产生一个特定的返回值(如果不是void类型的话),并将返回值返回给函数调用处。除了void类型外,需要返回值的函数,在其函数体中都应该包括后面带一个常量或变量的return语句,指明该函数的返回值。而void型函数则不需要带任何数据的return语句。
3.3.2 函数类型与参数传递
C++把函数返回值的数据类型规定为该函数的数据类型,其数据类型可以是预定义的、用户自定义的和派生类型(包括指针和引用类型等),但是数组和函数类型不能直接作为函数的返回类型。若要求返回函数返回一个数组或确定某一个可被执行的函数,这时可求助于指针,即返回指针类型。在默认情况下,函数的返回值类型为整型int。
为了终止函数的运行,将控制权交还给调用该函数的函数,可以使用return语句。Return语句有两种形式:
return;
return expression;
第一种形式用于void型函数,其作用是人为地终止当前函数的运行,返回调用它的函数,且不返回任何值。若无该语句,系统也会隐式地执行return语句,返回调用函数。
第二种形式的return语句需要指明函数返回值的表达式,用在需要返回值的函数中。其中expression可以是一个表达式,也可以是一个具有返回值的函数调用。如果一个函数实际返回值与函数定义时所说明的返回值类型不匹配,则系统在可能的情况下会使用隐式转换规则进行转换。如果不说明为void类型的函数中,示返回任何数值也不会导致编译失败,编译器只会给出警告。
在某些情况下,函数需要返回多个值,可以有三种处理方法:
(1)利用全局变量来实现函数多个计算结果的返回。其优点中比较简便,但是,其值的返回不直观,且破坏了函数界面明确与函数体内容相分离的要求。
(2)根据需要定义一个聚合型数据类型来容纳多个数值,将函数说明成返回该聚合数据类型。在这种情况下,可以令函数返回一个指向数组的指针或一个类的对象。该方法比较直观,且保持了函数界面的清晰,因而被广泛使用。
(3)将函数的参数定义成按址传递(指针或引用类型)而不是按值传递,利用这些按值传递的参数的改变来返回所需的结果。该方法仍然保持了函数界面的清晰,虽然值的返回不太直观,但其实现较为简单,因而被使用得最多。
大多数的函数需要一定数目的操作数,即函数参数。函数的参数在函数定义时用逗号进行分隔,放在函数名后的小括号中。既使不需要任何参数,函数定义时函数名后的小括号也是必需的。函数的参数表是函数之间进行通信的重要手段,它和函数的返回值一起,构成了函数的公共界面。函数的界面是函数之间进行通信的途径,只要保持函数的界面不变,即使函数体发生了变化,也不会影响调用该函数的其他函数,使得程序的可维护性增强。
注意:(1)、函数中参数表的参数必须单独指明其数据类型,不能认为一个数据类型说明符后的若干个参数都具有该数据类型。如:
double min(double val1,val2);//error
double min(double val2,double val2);
(2)、在同一个函数的定义与说明中,其参数表的组成及不同数据类型的参数的排列顺序必须相同,但是在参数表中不允许出现相同的参数变量名。在函数的定义中各参数必须有相应的变量名,在函数体中通过参数名来处理;而在函数说明中则只需要指明函数的各个参数的数据类型,变量名不是必须的。
在需要的时候,函数也可以不明确确定传约束它的参数类型和参数个数,而在参数表中仅用省略符“...”,即有0个或多个参数,且参数的数据类型未知。出现参数省略时,参数的数目及数据类型都由进行函数调用时传入的实参决定。在参数表中出现省略符有两种形式:
type function(arg-list,…);
type function(…);
如C++的标准输出函数void printf(const char*…)就是一个省略符的函数。
在某些函数中,可以为其参数表中的某些参数指定缺省值。缺省值是指那些虽然不是绝对确定,但在大多数情况下都有使用该值的参数设置。一个函数可以在形式参数中使用初始化变量的形式来定义缺省值。如:
char * screeninit(int height=24,int width=80,char background=’-‘);
以下几种调用方式都是合法的:
char *cursor;
cursor=screeninit();
cursor=screeninit(48);
cursor=screeninit(48,96,’#’);
注意:在带缺省值的参数表中,所有带缺值的参数必须话参数表的最后,且在程序的一个源文件中,某个函数的一个参数只能在一次说明中指定它的缺省值,否则会造成编译错误。
定义函数时,在参数中所描述的函数参数称为形式参数。在函数调用处,函数名后的括号中的常量、变量或表达式的值称为函数的实际参数。函数调用的参数传递过程就是用实际参数来对形式参数进行初始化。
C++中参数传递方式有三种:
(1)、按值传递方式(默认方式)。在该方式下,系统将各实际参数的值复制给形式参数,在函数体中对形式参数的修改并不会对实际参数产生任何影响,且它们在函数返回时自动消亡。因而没有必要对实际参数进行备份。
按值传递方式并不适合所有的函数参数传递。如较大的对象作为实际参数传递给函数或作为返回值传递给函数调用处。因为需要消耗大量的存储空间和运行时间。
(2)、按址传递方式。该方式需要将形式参数说明为指针,与之相应,在函数误用时对应的实际参数也是一指针变量。它们都有指向同一个内存地址,且该内存地址所在的内存区域并非为被调用函数的运行而专门开辟的。在函数调用完毕之后并不消亡。这种方式节约了时间和空间。
(3)、将形式参数说明为引用类型,即在形式参数的数据类型和变量名之间加一个取址操作符,如:
void inverse(double &val);
引用型参数将实参作为左值传递给函数,使得在函数内对参数的值的修改不再作用于局部拷贝,而是针对实参进行,也无需复制整个对象,节约了时间和空间。
注意:①、当引用型参数是整型或浮点型时,实参和形参的类型不匹配时,系统会自动产生一个符合类型匹配规则的临时对象,并将实参的右值赋给它,然后再由它传给函数,这并没有反映到实参的变化上,容易造成混乱。编译会给出警告信息。
②、在用数组作为函数参数时,进行传递的是该数组的首元素的指针,因而它是按址传递的。此外数组的元素个数并不是函数参数类型的一部分(未知),因而容易发生错误,应该引起高度重视。
3.3.3 内联函数在程序中对函数进行调用时有两种处理方法:
(1)、对于普通函数,系统将在函数调用处设置断点,并将当前函数的现场保留起来,将控制权交给被激活的函数,当被调用函数运行完毕返回时,再恢复被保留起来的现场,从调用函数的断点处继续往下运行。
(2)、对于说明为inline类型的内联函数,则在程序编译时,编译器会将该函数在调用点进行展开,使该函数的函数体部分作为调用函数的一部分溶入调用函数中。因此进行内联函数调用时不需要保留现场,也不会发生控制权转移,因此可降低开销。
内联函数是在编译时能够在函数的调用点处进行展开的函数。对内联函数的定义是在普通函数的定义之前加一个标识符inline,如:
inline int min(int va,int vb)
{
return (va<vb?va:vb);
}
使用内联函数主要基于以下几点考虑:
(1)、使用内联函数比直接在程序中书写相应代码具有更好的可读性。
(2)、从程序的可维护性的角度上看,使用内联函数便于程序的修改,使程序具有更好的可维护性和可移植性。
(3)、采用内联函数,可以利用编译器对函数参数的强制型类型检查,以尽早发现参数类型不匹配等问题,减少出错可能。
(4)、采用内联函数容易被其他函数引用而无需重新书写代码,增加程序的可重用性。
缺点是运行速度较慢,不能滥用,对于很长的函数没有必要使用内联函数。
3.3.4 递归调用如果在一个函数体内,包含了对其自身直接或间接的调用,该函数就称为递归调用。如以下是一个返回两个整型数的最大公约数的递归函数:
rgcd(int v1,int v2)
{
if (v2= =0) return v1;
return rgcd(v2,v1%v2);
}
注意:(1)、递归函数应有一个终止条件,使得函数的递归调用有可能跳出来,否则会导致程序的崩溃。
(2)、递归函数不能定义成内联型的。
优点:在某些情况下,使用递归函数使程序的代码清晰明确。
缺点:运行速度慢。
3.3.5 函数重载调用如果一个标识符具有两个或两个以上的意义,就称为生载。对于重载的标识符,其在任意一个特定环境下所表述的意义必须由其所处的上下文来决定。
C++中的函数重载是指两个或两个以上的函数可以拥有相同的函数名,用以区分和唯一标识它们的是它们具有不同的参数表(参数个数的不同,某些参数的数据类型不同)。例如:
int max(int val1,int val2);
float max(float val1,float val2);
double max(double val1,double val2);
利用函数的重载大大地方便了用于对函数的使用:用户只需要根据所要实现的功能的不同选择不同的函数名,然后根据应用的需要选择合适的参数类型,而不需要针对不同类型的参数再去选择不同的名字的函数。这样可以使程序员摆脱词汇的复杂性,增加程序的可读性。
当一个函数名在程序中被多次说明时,编译器会按照以下规则来处理从第二个说明以后的各个说明:
(1)、如果所说明的函数的返回值类型和参数表与已说明过的同名函数相同,则新说明的函数为原函数的重新简明,它们指的是同一个函数。注意,对同一个函数,可以多次说明,但在该函数的作用域内只能定义一次。
(2)、如果所说明的函数的参数表与已说明过的同名函数相同,但它们的返回值类型不同,则新说明的函数将被认为是错误的重新说明,编译将不能通过。
(3)、如果所说明的函数的参数表与已说明过的所有同名函数的参数表都不同,无论它们在参数个数或者对应参数的数据类型上存在差异,该函数都将被认为是对同名函数的重载。
例如:
float function(float var,int power);//函数首次说明
float function(float a,int b);//函数名、返回值类型及参数表相同,符合规则一
double function(float a,int b);// 函数名、参数表相同,但返回值类型不同,符合规则二
float function(float a,int b,int c);//参数表的参数个数不同,属于函数重载
float function(int a,int b);//参数表中对应参数的数据类型不同,属于函数重载
函数重载机制使得程序员在处理具有相似功能而以需要适应不同处理参数的一组函数时更为方便——起一个具有通用特定语义的函数名。但是,对于函数重载的使用也要根据实际应用的需要决定,不能滥用。
在进行函数调用时,系统会根据对重载函数进行调用时传入的实际参数的数目和数据类型,通过参数匹配机制对应一个具体的函数实例,并调用该函数实例所规定的操作。重载函数的参数匹配可能有三种情况出现:
(1)、匹配成功:可以为函数调用确定唯一的一个重载函数实例。此时系统会将各实际参数传给所确定的函数实例的形式参数,并执行规定的操作。
(2)匹配失败:在所有的同名函数的重载函数中,无法找到可以与函数调用实际参数相匹配的函数实例。这将导致编译失败。
(3)匹配不唯一:实际参数可以与多个函数的实例匹配。
对于函数重载,匹配可以由下列三种形式实现,并且在一种形式匹配不成功时按下面所列的顺序使用下一种形式再进行匹配。
(1)、完全匹配:实际参数的类型与某个函数重载实例的形式参数的类型完全相同。
(2)、通过应用系统中的标准类型转换进行匹配。如果找不到完全匹配的函数实例,系统将尝试着使用标准类型转换来对实际参数进行处理。
(3)、使用由用户自定义的类型转换进行匹配。如果在完全匹配失败并且在使用标准类型转换后也无法进行匹配时,系统尝试使用由用户自定义的类型转换。
3.3.6 函数指针在程序运行时,函数与变量都是在内存中进行存储的,但是函数的物理地址是函数的入口地址。而指针所保存的便是内存中的物理地址,因此函数的物理地址也可以赋给一个指针,称为函数指针。在定义了函数指针后,便可以利用函数指针来调用函数。
函数指针的引入是在保证不使最常用方法复杂化的前提下,提供了极大的灵活性。在某些情况下,程序根据实际的需要调用不同的功能函数。这时候可以有两种方法来实现:
(1)、使用switch开关分支语句来决定所调用的函数。
(2)、使用指针数组,将有可能需要执行的各函数的地址赋给数组中的每一个指针,然后根据实际情况确定数组的下标,通过函数指针来激活相应的函数。
利用第二种方法,程序的运行速度相对较快,程序的可扩展性和灵活性都较好。
[例3.5] 函数指针的定义与使用:一个简单的菜单选择系统(EX3_5)。
一个函数的类型由它的返回值和参数表共同决定,与之相对应,一个指向函数的指针也必须与其指向的函数有共同的返回值和参数表。定义函数指针的语法形式如下:
data_type (*variable)(type1,type2,.,,,typen);
其中data_type是函数指针所指向的函数的返回值类型,“*”是取值操作符,variable是函数指针名,type1~typen是函数指针所指的函数的各个参数类型。
注意:在函数指针的说明中,包含取值操作符“*”和函数指针名的小括号不能省略,否则定义语句的意义发生改变。如:
double *pf(int *,double,char *);//定义一个返回值为双精度型指针的函数。
double (*pf)(int *,double,char *);//定义一个函数指针,它所指向的函数具有双精度的返回值。
在前面的讨论中已经知道,如果不带下标,单独的数组名将被作为一个指向该数组占用内存区域的首地址的指针。函数名与数组名类似,它就是一个指向该函数的函数指针。而函数名较为特殊的是,在其前面使用取值操作符“&”后得到和仍然是一个指向该 函数的指针,且它们具有相同的数据类型。在使用函数指针调用函数之前必须对该指针进行初始化(在定义时进行或用赋值语句进行)。
注意:对于函数指针的初始化和赋值,只有为其赋值的函数名或函数指针与被赋值的函数指针具有相同的类型,所指向的函数具有相同的返回值以及参数表匹配时,才有可能进行赋值,否则将会导致编译错误。
例如:
extern int min(int *,int);
extern void (*pfv)(int *,int,int)=0;
extern int (*pfi)(int *,int)=0;
main()
{
pfi=min;//corect
pfv=min;//error
pfv=pfi;//error
}
函数指针还可以用0来对其赋值,表明该函数指针还没有指向任何函数。
函数指针还可以用来指向重载函数。当函数名表示一个重载函数时编译器会自动寻找返回值类型及参数表与之完全匹配的函数实例,并取其开始地址作为函数指针的值。如果不能完全匹配,该初始化或赋值语句将导致一个编译错误。
在使用函数指针来进行函数调用时在函数指针名前面可以加取值操作符“*”,也可以不加,而是直接用函数指针名后跟着包括在小括号中的实际参数表来进行调用。
例如:pfi(data,num); 等价于 (*pfi)(data,num);
也可以定义元素是函数指针的数组,利用其数组元素实现特定功能函数的调用。函数指针数组的定义及使用的一些相关知识与函数指针相类似,函数指针还可以作为函数的参数或返回值,具体可参考函数一节及例3.4
3.3.1 函数定义与调用函数是程序内部为完成特定功能而构造的独立的程序块。每个函数都具有独立的功能,明确的界面,形成清晰的模块结构,与程序的其他部分分隔开来。
函数定义的语法结构如下:
store_type data_type name(type1 data1,type data2,…..,typen datan)
{
statements;
}
其中store_type是函数的存储类型,它是可省略的;data_type是函数的返回值类型;name是函数名,用来标识函数;小括号括起来的一组数据类型和参数构成了函数的参数表(形式参数),其中type1~typen是n个参数的变量名;被大括号括起来的一组语句statements是执行相应功能的函数体。C++规定,函数的定义不能嵌套,即一个函数的内部不能定义其他函数,每个函数都是相对独立的程序块。各函数定义的顺序是任意的,不影响程序运行时函数调用的顺序。
如:
inline double abs(double val)
{
return (val<0?-val:val);
}
inline double max(double val1,double val2)
{
return (val1>val2)?val1:val2);
}
一个程序内的函数,即使其作用域的重叠,也可以是同名的,但同名函数的参数表必须不能完全相同。在函数调用时,编译器会根据函数名及参数的类型及数目自动确定调用哪一个函数。函数必须先定义或说明后使用,否则会导致编译错误。在一个程序内,函数必须且只能定义一次,但可以被多次说明。函数说明时只需要包括函数的返回值类型、函数名和参数表,这三项组成了函数原型。例如上面列举的确良个函数的函数原型分别为
double abs(double);
double max(double,double);
调用函数的形式是在函数名后加一对小括号,小括号内包含有逗号分隔开的相应数目的参数(称为实参数)——常量或变量。在函数调用时,编译器将各个实参数有值分另代入形式参数中(称为参数传递),并利用这些值进行函数体所规定的计算或其他操作,产生一个特定的返回值(如果不是void类型的话),并将返回值返回给函数调用处。除了void类型外,需要返回值的函数,在其函数体中都应该包括后面带一个常量或变量的return语句,指明该函数的返回值。而void型函数则不需要带任何数据的return语句。
3.3.2 函数类型与参数传递
C++把函数返回值的数据类型规定为该函数的数据类型,其数据类型可以是预定义的、用户自定义的和派生类型(包括指针和引用类型等),但是数组和函数类型不能直接作为函数的返回类型。若要求返回函数返回一个数组或确定某一个可被执行的函数,这时可求助于指针,即返回指针类型。在默认情况下,函数的返回值类型为整型int。
为了终止函数的运行,将控制权交还给调用该函数的函数,可以使用return语句。Return语句有两种形式:
return;
return expression;
第一种形式用于void型函数,其作用是人为地终止当前函数的运行,返回调用它的函数,且不返回任何值。若无该语句,系统也会隐式地执行return语句,返回调用函数。
第二种形式的return语句需要指明函数返回值的表达式,用在需要返回值的函数中。其中expression可以是一个表达式,也可以是一个具有返回值的函数调用。如果一个函数实际返回值与函数定义时所说明的返回值类型不匹配,则系统在可能的情况下会使用隐式转换规则进行转换。如果不说明为void类型的函数中,示返回任何数值也不会导致编译失败,编译器只会给出警告。
在某些情况下,函数需要返回多个值,可以有三种处理方法:
(1)利用全局变量来实现函数多个计算结果的返回。其优点中比较简便,但是,其值的返回不直观,且破坏了函数界面明确与函数体内容相分离的要求。
(2)根据需要定义一个聚合型数据类型来容纳多个数值,将函数说明成返回该聚合数据类型。在这种情况下,可以令函数返回一个指向数组的指针或一个类的对象。该方法比较直观,且保持了函数界面的清晰,因而被广泛使用。
(3)将函数的参数定义成按址传递(指针或引用类型)而不是按值传递,利用这些按值传递的参数的改变来返回所需的结果。该方法仍然保持了函数界面的清晰,虽然值的返回不太直观,但其实现较为简单,因而被使用得最多。
大多数的函数需要一定数目的操作数,即函数参数。函数的参数在函数定义时用逗号进行分隔,放在函数名后的小括号中。既使不需要任何参数,函数定义时函数名后的小括号也是必需的。函数的参数表是函数之间进行通信的重要手段,它和函数的返回值一起,构成了函数的公共界面。函数的界面是函数之间进行通信的途径,只要保持函数的界面不变,即使函数体发生了变化,也不会影响调用该函数的其他函数,使得程序的可维护性增强。
注意:(1)、函数中参数表的参数必须单独指明其数据类型,不能认为一个数据类型说明符后的若干个参数都具有该数据类型。如:
double min(double val1,val2);//error
double min(double val2,double val2);
(2)、在同一个函数的定义与说明中,其参数表的组成及不同数据类型的参数的排列顺序必须相同,但是在参数表中不允许出现相同的参数变量名。在函数的定义中各参数必须有相应的变量名,在函数体中通过参数名来处理;而在函数说明中则只需要指明函数的各个参数的数据类型,变量名不是必须的。
在需要的时候,函数也可以不明确确定传约束它的参数类型和参数个数,而在参数表中仅用省略符“...”,即有0个或多个参数,且参数的数据类型未知。出现参数省略时,参数的数目及数据类型都由进行函数调用时传入的实参决定。在参数表中出现省略符有两种形式:
type function(arg-list,…);
type function(…);
如C++的标准输出函数void printf(const char*…)就是一个省略符的函数。
在某些函数中,可以为其参数表中的某些参数指定缺省值。缺省值是指那些虽然不是绝对确定,但在大多数情况下都有使用该值的参数设置。一个函数可以在形式参数中使用初始化变量的形式来定义缺省值。如:
char * screeninit(int height=24,int width=80,char background=’-‘);
以下几种调用方式都是合法的:
char *cursor;
cursor=screeninit();
cursor=screeninit(48);
cursor=screeninit(48,96,’#’);
注意:在带缺省值的参数表中,所有带缺值的参数必须话参数表的最后,且在程序的一个源文件中,某个函数的一个参数只能在一次说明中指定它的缺省值,否则会造成编译错误。
定义函数时,在参数中所描述的函数参数称为形式参数。在函数调用处,函数名后的括号中的常量、变量或表达式的值称为函数的实际参数。函数调用的参数传递过程就是用实际参数来对形式参数进行初始化。
C++中参数传递方式有三种:
(1)、按值传递方式(默认方式)。在该方式下,系统将各实际参数的值复制给形式参数,在函数体中对形式参数的修改并不会对实际参数产生任何影响,且它们在函数返回时自动消亡。因而没有必要对实际参数进行备份。
按值传递方式并不适合所有的函数参数传递。如较大的对象作为实际参数传递给函数或作为返回值传递给函数调用处。因为需要消耗大量的存储空间和运行时间。
(2)、按址传递方式。该方式需要将形式参数说明为指针,与之相应,在函数误用时对应的实际参数也是一指针变量。它们都有指向同一个内存地址,且该内存地址所在的内存区域并非为被调用函数的运行而专门开辟的。在函数调用完毕之后并不消亡。这种方式节约了时间和空间。
(3)、将形式参数说明为引用类型,即在形式参数的数据类型和变量名之间加一个取址操作符,如:
void inverse(double &val);
引用型参数将实参作为左值传递给函数,使得在函数内对参数的值的修改不再作用于局部拷贝,而是针对实参进行,也无需复制整个对象,节约了时间和空间。
注意:①、当引用型参数是整型或浮点型时,实参和形参的类型不匹配时,系统会自动产生一个符合类型匹配规则的临时对象,并将实参的右值赋给它,然后再由它传给函数,这并没有反映到实参的变化上,容易造成混乱。编译会给出警告信息。
②、在用数组作为函数参数时,进行传递的是该数组的首元素的指针,因而它是按址传递的。此外数组的元素个数并不是函数参数类型的一部分(未知),因而容易发生错误,应该引起高度重视。
3.3.3 内联函数在程序中对函数进行调用时有两种处理方法:
(1)、对于普通函数,系统将在函数调用处设置断点,并将当前函数的现场保留起来,将控制权交给被激活的函数,当被调用函数运行完毕返回时,再恢复被保留起来的现场,从调用函数的断点处继续往下运行。
(2)、对于说明为inline类型的内联函数,则在程序编译时,编译器会将该函数在调用点进行展开,使该函数的函数体部分作为调用函数的一部分溶入调用函数中。因此进行内联函数调用时不需要保留现场,也不会发生控制权转移,因此可降低开销。
内联函数是在编译时能够在函数的调用点处进行展开的函数。对内联函数的定义是在普通函数的定义之前加一个标识符inline,如:
inline int min(int va,int vb)
{
return (va<vb?va:vb);
}
使用内联函数主要基于以下几点考虑:
(1)、使用内联函数比直接在程序中书写相应代码具有更好的可读性。
(2)、从程序的可维护性的角度上看,使用内联函数便于程序的修改,使程序具有更好的可维护性和可移植性。
(3)、采用内联函数,可以利用编译器对函数参数的强制型类型检查,以尽早发现参数类型不匹配等问题,减少出错可能。
(4)、采用内联函数容易被其他函数引用而无需重新书写代码,增加程序的可重用性。
缺点是运行速度较慢,不能滥用,对于很长的函数没有必要使用内联函数。
3.3.4 递归调用如果在一个函数体内,包含了对其自身直接或间接的调用,该函数就称为递归调用。如以下是一个返回两个整型数的最大公约数的递归函数:
rgcd(int v1,int v2)
{
if (v2= =0) return v1;
return rgcd(v2,v1%v2);
}
注意:(1)、递归函数应有一个终止条件,使得函数的递归调用有可能跳出来,否则会导致程序的崩溃。
(2)、递归函数不能定义成内联型的。
优点:在某些情况下,使用递归函数使程序的代码清晰明确。
缺点:运行速度慢。
3.3.5 函数重载调用如果一个标识符具有两个或两个以上的意义,就称为生载。对于重载的标识符,其在任意一个特定环境下所表述的意义必须由其所处的上下文来决定。
C++中的函数重载是指两个或两个以上的函数可以拥有相同的函数名,用以区分和唯一标识它们的是它们具有不同的参数表(参数个数的不同,某些参数的数据类型不同)。例如:
int max(int val1,int val2);
float max(float val1,float val2);
double max(double val1,double val2);
利用函数的重载大大地方便了用于对函数的使用:用户只需要根据所要实现的功能的不同选择不同的函数名,然后根据应用的需要选择合适的参数类型,而不需要针对不同类型的参数再去选择不同的名字的函数。这样可以使程序员摆脱词汇的复杂性,增加程序的可读性。
当一个函数名在程序中被多次说明时,编译器会按照以下规则来处理从第二个说明以后的各个说明:
(1)、如果所说明的函数的返回值类型和参数表与已说明过的同名函数相同,则新说明的函数为原函数的重新简明,它们指的是同一个函数。注意,对同一个函数,可以多次说明,但在该函数的作用域内只能定义一次。
(2)、如果所说明的函数的参数表与已说明过的同名函数相同,但它们的返回值类型不同,则新说明的函数将被认为是错误的重新说明,编译将不能通过。
(3)、如果所说明的函数的参数表与已说明过的所有同名函数的参数表都不同,无论它们在参数个数或者对应参数的数据类型上存在差异,该函数都将被认为是对同名函数的重载。
例如:
float function(float var,int power);//函数首次说明
float function(float a,int b);//函数名、返回值类型及参数表相同,符合规则一
double function(float a,int b);// 函数名、参数表相同,但返回值类型不同,符合规则二
float function(float a,int b,int c);//参数表的参数个数不同,属于函数重载
float function(int a,int b);//参数表中对应参数的数据类型不同,属于函数重载
函数重载机制使得程序员在处理具有相似功能而以需要适应不同处理参数的一组函数时更为方便——起一个具有通用特定语义的函数名。但是,对于函数重载的使用也要根据实际应用的需要决定,不能滥用。
在进行函数调用时,系统会根据对重载函数进行调用时传入的实际参数的数目和数据类型,通过参数匹配机制对应一个具体的函数实例,并调用该函数实例所规定的操作。重载函数的参数匹配可能有三种情况出现:
(1)、匹配成功:可以为函数调用确定唯一的一个重载函数实例。此时系统会将各实际参数传给所确定的函数实例的形式参数,并执行规定的操作。
(2)匹配失败:在所有的同名函数的重载函数中,无法找到可以与函数调用实际参数相匹配的函数实例。这将导致编译失败。
(3)匹配不唯一:实际参数可以与多个函数的实例匹配。
对于函数重载,匹配可以由下列三种形式实现,并且在一种形式匹配不成功时按下面所列的顺序使用下一种形式再进行匹配。
(1)、完全匹配:实际参数的类型与某个函数重载实例的形式参数的类型完全相同。
(2)、通过应用系统中的标准类型转换进行匹配。如果找不到完全匹配的函数实例,系统将尝试着使用标准类型转换来对实际参数进行处理。
(3)、使用由用户自定义的类型转换进行匹配。如果在完全匹配失败并且在使用标准类型转换后也无法进行匹配时,系统尝试使用由用户自定义的类型转换。
3.3.6 函数指针在程序运行时,函数与变量都是在内存中进行存储的,但是函数的物理地址是函数的入口地址。而指针所保存的便是内存中的物理地址,因此函数的物理地址也可以赋给一个指针,称为函数指针。在定义了函数指针后,便可以利用函数指针来调用函数。
函数指针的引入是在保证不使最常用方法复杂化的前提下,提供了极大的灵活性。在某些情况下,程序根据实际的需要调用不同的功能函数。这时候可以有两种方法来实现:
(1)、使用switch开关分支语句来决定所调用的函数。
(2)、使用指针数组,将有可能需要执行的各函数的地址赋给数组中的每一个指针,然后根据实际情况确定数组的下标,通过函数指针来激活相应的函数。
利用第二种方法,程序的运行速度相对较快,程序的可扩展性和灵活性都较好。
[例3.5] 函数指针的定义与使用:一个简单的菜单选择系统(EX3_5)。
一个函数的类型由它的返回值和参数表共同决定,与之相对应,一个指向函数的指针也必须与其指向的函数有共同的返回值和参数表。定义函数指针的语法形式如下:
data_type (*variable)(type1,type2,.,,,typen);
其中data_type是函数指针所指向的函数的返回值类型,“*”是取值操作符,variable是函数指针名,type1~typen是函数指针所指的函数的各个参数类型。
注意:在函数指针的说明中,包含取值操作符“*”和函数指针名的小括号不能省略,否则定义语句的意义发生改变。如:
double *pf(int *,double,char *);//定义一个返回值为双精度型指针的函数。
double (*pf)(int *,double,char *);//定义一个函数指针,它所指向的函数具有双精度的返回值。
在前面的讨论中已经知道,如果不带下标,单独的数组名将被作为一个指向该数组占用内存区域的首地址的指针。函数名与数组名类似,它就是一个指向该函数的函数指针。而函数名较为特殊的是,在其前面使用取值操作符“&”后得到和仍然是一个指向该 函数的指针,且它们具有相同的数据类型。在使用函数指针调用函数之前必须对该指针进行初始化(在定义时进行或用赋值语句进行)。
注意:对于函数指针的初始化和赋值,只有为其赋值的函数名或函数指针与被赋值的函数指针具有相同的类型,所指向的函数具有相同的返回值以及参数表匹配时,才有可能进行赋值,否则将会导致编译错误。
例如:
extern int min(int *,int);
extern void (*pfv)(int *,int,int)=0;
extern int (*pfi)(int *,int)=0;
main()
{
pfi=min;//corect
pfv=min;//error
pfv=pfi;//error
}
函数指针还可以用0来对其赋值,表明该函数指针还没有指向任何函数。
函数指针还可以用来指向重载函数。当函数名表示一个重载函数时编译器会自动寻找返回值类型及参数表与之完全匹配的函数实例,并取其开始地址作为函数指针的值。如果不能完全匹配,该初始化或赋值语句将导致一个编译错误。
在使用函数指针来进行函数调用时在函数指针名前面可以加取值操作符“*”,也可以不加,而是直接用函数指针名后跟着包括在小括号中的实际参数表来进行调用。
例如:pfi(data,num); 等价于 (*pfi)(data,num);
也可以定义元素是函数指针的数组,利用其数组元素实现特定功能函数的调用。函数指针数组的定义及使用的一些相关知识与函数指针相类似,函数指针还可以作为函数的参数或返回值,具体可参考函数一节及例3.4