所在的位置: C++ >> C++市场 >> C编程函数高阶

C编程函数高阶

中国白癜风专家 https://m-mip.39.net/baidianfeng/mipso_4283860.html

函数是模块化编程思想的重要体现,相对于传统的C语言,C++还提供了很多新的函数特性。这一章我们就来深入探讨一下函数的高级用法以及在C++中的新特性。

内联函数

内联函数是C++为了提高运行速度做的一项优化。

函数让代码更加模块化,可重用性、可读性大大提高;不过函数也有一个缺点:函数调用需要执行一系列额外操作,会降低程序运行效率。

为了解决这个问题,C++引入了“内联函数”的概念。使用内联函数时,编译器不再去做常规的函数调用,而是把它在调用点上“内联”展开,也就是直接用函数代码替换了函数调用。

1.内联函数的定义

定义内联函数,只需要在函数声明或者函数定义前加上inline关键字。

例如之前写过的函数:比较两个字符串、并返回较长的那个,就可以重写为内联函数:

inlineconststringlongerStr(conststringstr1,conststringstr2)

{

returnstr1.size()str2.size()?str1:str2;

}

当我们试图打印输出调用结果时:

coutlongerStr(str1,str2)endl;

编译器会自动把它展开为:

cout(str1.size()str2.size()?str1:str2)endl;

这样就大大提高了运行效率。

2.内联函数和宏

内联函数是C++新增的特性。在C语言中,类似功能是通过预处理语句#define定义“宏”来实现的。

然而C中的宏本身并不是函数,无法进行值传递;它的本质是文本替换,我们一般只用宏来定义常量。用宏实现函数的功能会比较麻烦,而且可读性较差。所以在C++中,一般都会用内联函数来取代C中的宏。

默认实参

在有些场景中,当调用一个函数时它的某些形参一般都会被赋一个固定的值。为了简单起见,我们可以给它设置一个“默认值”,这样就不用每次都传同样的值了。

这种会反复出现的默认值,称为函数的“默认实参”。当调用一个有默认实参的函数时,这个实参可以省略。

1.定义带默认实参的函数

我们用一个string对象表示学生基本信息,调用函数时应传入学生的姓名、年龄和平均成绩。对于这些参数,我们可以指定默认实参:

//默认实参

stringstuInfo(stringname="",intage=18,doublescore=60)

{

stringinfo="学生姓名:"+name+"\t年龄:"+

to_string(age)+"\t平均成绩:"+to_string(score);

returninfo;

}

定义默认实参,形式上就是给形参做初始化。这里在整合学生信息时,使用了运算符+进行字符串拼接,并且调用to_string函数将age和score转换成了string。

这里需要注意,一旦某个形参被定义了默认实参,那它后面的所有形参都必须有默认实参。也就是说,所有默认实参的指定,应该在形参列表的末尾。

//错误,默认实参不在形参列表末尾

//stringstuInfo(stringname="",intage=18,doublescore);

//正确,可以前面的形参没有默认实参

stringstuInfo(stringname,intage=18,doublescore=60);

2.使用默认实参调用函数

函数调用时,如果对某个形参不传实参,那么它初始化时用的就是默认实参的值。由于之前所有形参都定义了默认实参,因此可以用不同的传参方式调用函数:

coutstuInfo()endl;//"",18,60.0

coutstuInfo("张三")endl;//"张三",18,60.0

coutstuInfo("李四",20)endl;//"李四",20,60.0

coutstuInfo("王五",22,85.5)endl;//"王五",22,85.5

//coutstuInfo(19,92.5)endl;//错误,不能跳过前面的形参给后面传值

//coutstuInfo(,,59.5)endl;//错误,只能省略末尾的形参

可以看到,默认实参定义时要优先放到形参列表的尾部;而调用时,只能省略尾部的参数,不能跳过前面的形参给后面传值。

函数重载

在C++中,同一作用域下,同一个函数名是可以定义多次的,前提是形参列表不同。这种名字相同但形参列表不同的函数,叫做“重载函数”。这是C++相对C语言的重大改进,也是面向对象的基础。

1.定义重载函数

在上一章数组形参部分,我们曾经实现过几个不同的打印数组的函数,它们是可以同时存在的:

//使用指针和长度作为形参

voidprintArray(constint*arr,intsize)

{

for(inti=0;isize;i++)

coutarr[i]"\t";

coutendl;

}

//使用数组引用作为形参

voidprintArray(constint(arr)[6])

{

for(intnum:arr)

coutnum"\t";

coutendl;

}

intmain()

{

intarr[6]={1,2,,4,5,6};

printArray(arr,6);//传入两个参数,调用第一种实现

printArray(arr);//传入一个参数,调用第二种实现

}

这里需要注意:

l重载的函数,应该在形参的数量或者类型上有所不同;

l形参的名称在类型中可以省略,所以只有形参名不同的函数是一样的;

l调用函数时,编译器会根据传递的实参个数和类型,自动推断使用哪个函数;

l主函数不能重载

2.有const形参时的重载

当形参有const修饰时,要区分它对于实参的要求到底是什么,是否要进行值的拷贝。如果是传值参数,传入实参时会发生值的拷贝,那么实参是变量还是常量其实是没有区别的:

voidfun(intx);

voidfun(constintx);//int常量做形参,跟不加const等价

voidfun2(int*p);

voidfun2(int*constp);//指针常量做形参,也跟不加const等价

这种情况下,const不会影响传入函数的实参类型,所以跟不加const的定义是一样的;这叫做“顶层const”。这时两个函数相同,无法进行函数重载。

另一种情况则不同,那就是传引用参数。这时如果有const修饰,就成了“常量的引用”;对于一个常量,只能用常量引用来绑定,而不能使用普通引用。

类似地,对于一个常量的地址,只能由“指向常量的指针”来指向它,而不能用普通指针。

voidfun(intx);

voidfun(constintx);//形参类型是常量引用,这是一个新函数

voidfun4(int*p);

voidfun4(constint*p);//形参类型是指向常量的指针,这是一个新函数

这种情况下,const限制了间接访问的数据对象是常量,这叫做“底层const”。当实参是常量时,不能对不带const的引用进行初始化,所以只能调用常量引用做形参的函数;而如果实参是变量,就会优先匹配不带const的普通引用:这就实现了函数重载。

.函数匹配

如果传入的实参跟形参类型不同,只要能通过隐式类型转换变成需要类型,函数也可以正确调用。那假如有几个不同的重载函数,它们的形参类型可以进行自动转换,这时传入实参应该调用哪个函数呢?例如:

voidf();

voidf(intx);

voidf(intx,inty);

voidf(doublex,doubley=1.5);

f(.14);//应该调用哪个函数?

确定到底调用哪个函数的过程,叫做“函数匹配”。

(1)候选函数

函数匹配的第一步,就是确定“候选函数”,也就是先找到对应的重载函数集。候选函数有两个要求:

与调用的函数同名

函数的声明,在函数的调用点是可见的

所以上面的例子中,一共有4个叫做f的函数,它们都是候选函数。

(2)可行函数

接下来需要从候选函数中,选出跟传入的实参匹配的函数。这些函数叫做“可行函数”。可行函数也有两个要求:

形参个数与调用传入的实参数量相等

每个实参的类型与对应形参的类型相同,或者可以转换成形参的类型

上面的例子中,传入的实参只有一个,是一个double类型的字面值常量,所以可以排除f()和f(int,int)。而剩下的f(int)和f(double,double=1.5)都是匹配的,所以有2个可行函数。

()寻找最佳匹配

最后就是在可行函数中,选择最佳匹配。简单来说,实参类型与形参类型越接近,它们就匹配得越好。所以,能不进行转换就实际匹配的,要优于需要转换的。

上面的例子中,f(int)必须要将double类型的实参转换成int,而f(double,double=1.5)不需要,所以后者是最佳匹配,最终调用的就是它。第二个参数会由默认实参1.5来填补。

(4)多参数的函数匹配

如果实参的数量不止一个,那么就需要逐个比较每个参数;同样,类型能够精确匹配的要优于需要转换的。这时寻找最佳匹配的原则如下:

如果可行函数的所有形参都能精确匹配实参,那么它就是最佳匹配

如果没有全部精确匹配,那么当一个可行函数所有参数的匹配,都不比别的可行函数差、并且至少有一个参数要更优,那它就是最佳匹配

(5)二义性调用

如果检查所有实参之后,有多个可行函数不分优劣、无法找到一个最佳匹配,那么编译器会报错,这被称为“二义性调用”。例如:

f(10,.14);//二义性调用

这时的可行函数为f(int,int)和f(double,double=1.5)。第一个实参为int类型,f(int,int)占优;而第二个实参为double类型,f(double,double=1.5)占优。这时两个可行函数分不出胜负,于是就会报二义性调用错误。

4.重载与作用域

重载是否生效,跟作用域是有关系的。如果在内层、外层作用域分别声明了同名的函数,那么内层作用域中的函数会覆盖外层的同名实体,让它隐藏起来。

不同的作用域中,是无法重载函数名的。

#includeiostream

usingnamespacestd;

voidprint(doubled)

{

cout"d:"dendl;

}

voidprint(strings)

{

cout"s:"sendl;

}

intmain()

{

//调用之前做函数声明

voidprint(inti);

print(10);

print(.14);//将.14转换为,然后调用

//print("hello");//错误,找不到对应参数的函数定义

cin.get();

}

voidprint(inti)

{

cout"i:"iendl;

}

如果想让函数正确地重载,应该把函数声明放到同一作用域下:

#includeiostream

usingnamespacestd;

//作用域和重载测试

voidprint(inti)

{

cout"i:"iendl;

}

voidprint(doubled)

{

cout"d:"dendl;

}

voidprint(strings)

{

cout"s:"sendl;

}

intmain()

{

print(10);

print(.14);

print("hello");

cin.get();

}

函数指针

一类特殊的指针,指向的不是数据对象而是函数,这就是“函数指针”。

1.声明函数指针

函数指针本质还是指针,它的类型和所指向的对象类型有关。现在指向的是函数,函数的类型是由它的返回类型和形参类型共同决定的,跟函数名、形参名都没有关系。

例如之前写过的函数:

stringstuInfo(stringname="",intage=18,doublescore=60)

{

stringinfo="学生姓名:"+name+"\t年龄:"+

to_string(age)+"\t平均成绩:"+to_string(score);

returninfo;

}

它的类型就是:string(string,int,double)。

如果要声明一个指向它的指针,只要把原先函数名的位置填上指针就可以了:

string(*fp)(string,int,double);//一个函数指针

这里要注意,指针两侧的括号必不可少。如果去掉括号,

string*fp(string,int,double);//这是一个函数,返回值为指向string的指针

这就变成了一个返回string*类型的函数。

更加复杂的例子也是一样,例如之前写过的比较字符串长度的函数:

conststringlongerStr(conststringstr1,conststringstr2)

{

returnstr1.size()str2.size()?str1:str2;

}

对应类型的函数指针就是:

conststring(*fp)(conststring,conststring);

2.使用函数指针

当一个函数名后面跟调用操作符(小括号),表示函数调用;而单独使用函数名作为一个值时,函数会自动转换成指针。这一点跟数组名类似。

所以我们可以直接使用函数名给函数指针赋值:

fp=longerStr;//直接将函数名作为指针赋给fp

fp=longerStr;//取地址符是可选的,和上面没有区别

也可以加上取地址符,这和不加是等价的。

赋值之后,就可以通过fp调用函数了。fp做解引用可以得到函数,而这里解引用符*也是可选的,不做解引用同样可以直接表示函数。

coutfp("hello","world")endl;

cout(*fp)("C++","isgood")endl;

所以这里能够看出,函数指针完全可以当做函数来使用。

在对函数指针赋值时,函数的类型必须精确匹配。当然,函数指针也可以赋nullptr,表示空指针,没有指向任何一个函数。

.函数指针作为形参

有了指向函数的指针,就给函数带来了更加丰富灵活的用法。比如,可以将函数指针作为形参,定义在另一个函数中。也就是说,可以定义一个函数,它以另一个函数类型作为形参。当然,函数本身不能作为形参,不过函数指针完美地填补了这个空缺。这一点上,函数跟数组非常类似。

voidselectStr(conststrings1,conststrings2,conststringfp(conststring,conststring));

voidselectStr(conststrings1,conststrings2,conststring(*fp)(conststring,conststring));

同样,上面两种形式是等价的,*是可选的。

很明显,对于函数类型和函数指针类型来说,这样的定义太过复杂,所以有必要使用typedef做一个类型别名的声明。

//类型别名

typedefconststringFunc(conststring,conststring);//函数类型

typedefconststring(*FuncP)(conststring,conststring);//函数指针类型

当然,还可以用C++11提供的decltype函数直接获取类型,更加简洁:

typedefdecltype(longerStr)Func2;

typedefdecltype(longerStr)*FuncP2;

这样一来,声明函数指针做形参的新函数,就非常方便了:

voidselectStr(conststring,conststring,Func);

4.函数指针作为返回值

类似地,函数不能直接返回另一个函数,但是可以返回函数指针。所以可以将函数指针作为另一个函数的返回值。

这里需要注意的是,这种场景下,函数的返回类型必须是函数指针,而不能是函数类型。

//函数指针作为返回值

FuncPfun(int);

//Funcfun2(int);//错误,不能直接返回函数

Func*fun2(int);

//尾置返回类型

autofun(int)-FuncP;

另外也可以使用尾置返回类型的方式,指定返回函数指针类型。




转载请注明:http://www.aierlanlan.com/rzdk/6458.html

  • 上一篇文章:
  •   
  • 下一篇文章: 没有了