您当前的位置:首页 > 计算机 > 编程开发 > VC/VC++

C++函数重载过程中的二义性和类型转换

时间:01-05来源:作者:点击数:

上节我们讲到,发生函数调用时编译器会根据传入的实参的个数、类型、顺序等信息去匹配要调用的函数,这在大部分情况下都能够精确匹配。但当实参的类型和形参的类型不一致时情况就会变得稍微复杂,例如函数形参的类型是int,调用函数时却将short类型的数据交给了它,编译器就需要先将short类型转换为int类型才能匹配成功。

现在有以下几种形式的函数重载(例1):

#include <iostream>
using namespace std;
//1号函数
void func(char ch){
    cout<<"#1"<<endl;
}
//2号函数
void func(int n){
    cout<<"#2"<<endl;
}
//3号函数
void func(long m){
    cout<<"#3"<<endl;
}
//4号函数
void func(double f){
    cout<<"#4"<<endl;
}
int main(){
    short s = 99;
    float f = 84.6;
   
    func('a');  //不需要类型转换,调用func(char)
    func(s);  //将short转换成int,调用func(int)
    func(49);  //不需要类型转换,调用func(int)
    func(f);  //将float转换成double,调用func(double)
    return 0;
}

运行结果:

#1
#2
#2
#4

这段代码很容易理解,相信大家都不会有什么疑问。对代码稍作修改,将2号函数void func(int n)去掉(例2):

#include <iostream>
using namespace std;
//1号函数
void func(char ch){
    cout<<"#1"<<endl;
}
//3号函数
void func(long m){
    cout<<"#3"<<endl;
}
//4号函数
void func(double f){
    cout<<"#4"<<endl;
}
int main(){
    short s = 99;
    float f = 84.6;
  
    func('a');
    func(s);
    func(49);
    func(f);
    return 0;
}

这段代码在编译时发生了错误,大概的意思是:func(s)func(49)这两个函数发生调用错误,它们可以匹配三个重载函数中的任何一个,编译器不知道如何抉择。

这着实有点让人摸不着头脑!根据以往的编程经验,s 和 49 不都应该被转换成 long 类型,从而匹配3号函数void func(long m)吗?这种推论在一般的函数调用或者四则运算中确实没错,但它不一定适用于重载函数!

C++ 标准规定,在进行重载决议时编译器应该按照下面的优先级顺序来处理实参的类型:

优先级 包含的内容 举例说明
精确匹配 不做类型转换,直接匹配 (暂无说明)
只是做微不足道的转换 从数组名到数组指针、从函数名到指向函数的指针、从非 const 类型到 const 类型。
类型提升后匹配 整型提升 从 bool、char、short 提升为 int,或者从 char16_t、char32_t、wchar_t 提升为 int、long、long long。
小数提升 从 float 提升为 double。
使用自动类型转换后匹配 整型转换 从 char 到 long、short 到 long、int 到 short、long 到 char。
小数转换 从 double 到 float。
整数和小数转换 从 int 到 double、short 到 float、float 到 int、double 到 long。
指针转换 从 int * 到 void *。

C++ 标准还规定,编译器应该按照从高到低的顺序来搜索重载函数,首先是精确匹配,然后是类型提升,最后才是类型转换;一旦在某个优先级中找到唯一的一个重载函数就匹配成功,不再继续往下搜索。

如果在一个优先级中找到多个(两个以及以上)合适的重载函数,编译器就会陷入两难境地,不知道如何抉择,编译器会将这种模棱两可的函数调用视为一种错误,因为这些合适的重载函数同等“优秀”,没有一个脱颖而出,调用谁都一样。这就是函数重载过程中的二义性错误。

在例1中,func('a')func(49)分别和void func(char)void func(int)精确匹配;func(s)没有精确匹配的重载函数,编译器将 s 的类型提升为 int 后和void func(int)匹配成功;func(f)也是类似的道理,将 f 提升为 double 类型后和void func(double)匹配成功。

在例2中,func(s)func(49)没有精确匹配的重载函数,将它们的类型都提升为 int 后仍然不能匹配,接下来进入自动类型转换阶段,发现 s 被转换为 char(整型转换)、long(整型转换)、double(整数和小数转换)后都有比较合适的函数,而且它们在同一个优先级中,谁也不比谁优秀,调用哪个都一样,产生了二义性,所以编译器会报错。

注意,类型提升和类型转换不是一码事!类型提升是积极的,是为了更加高效地利用计算机硬件,不会导致数据丢失或精度降低;而类型转换是不得已而为之,不能保证数据的正确性,也不能保证应有的精度。类型提升只有上表中列出的几种情况,其他情况都是类型转换。

多个参数时的二义性

当重载函数有多个参数时也会产生二义性,而且情况更加复杂。C++ 标准规定,如果有且只有一个函数满足下列条件,则匹配成功:

  • 该函数对每个实参的匹配都不劣于其他函数;
  • 至少有一个实参的匹配优于其他函数。

假设现在有以下几个函数原型:

void func(int, int);  //①
void func(char, int, float);  //②
void func(char, long, double);  //③

我们来分析如下的调用会发生什么情况:

short n = 99;
func('@', n, 99);
func('@', n, 99.5);

函数原型func(int, int)只有两个参数,而函数调用有三个参数,很容易看出来不匹配,在初次筛选时就会被过滤掉,接下来我们只讨论②③个函数原型。

1) 先来看第一个函数调用。如果只考虑第一个实参'@',那么②③两个函数都能够精确匹配,谁也不比谁优秀,是平等的;如果只考虑第二个实参n,对于②,需要把 short 提升为 int(类型提升),对于③,需要把 short 转换为 long(类型转换),类型提升的优先级高于类型转换,所以②胜出;如果只考虑第三个实参99,②③都要进行类型转换,没有哪一个能胜出,它们是平等的。

从整体上看,②③在第一、三个实参的匹配中是平等的,但②在第二个实参的匹配中胜出,也就是说,②对每个实参的匹配都不劣于③,但有一个实参的匹配优于③,所以②最终脱颖而出,成为被调用函数。

2) 再来看第二个函数调用。只考虑第一个实参时②③是平等的,没有谁胜出;只考虑第二个实参时②胜出;只考虑第三个实参时,②需要类型转换,③能够精确匹配,精确匹配的优先级高于类型转换,所以③胜出。

从整体上看,②③在第一个实参的匹配中是平等的,②在第二个实参的匹配中胜出,③在第三个实参的匹配中胜出,它们最终“打成了平手”,分不清孰优孰劣,所以编译器不知道如何抉择,会产生二义性错误。

总结

在设计重载函数时,参数类型过少或者过多都容易引起二义性错误,因为这些类型相近,彼此之间会相互转换。

例如我们要设计几个重载函数来处理数值,数值包括小数和整数,如果只提供了以下两个函数原型:

void func(int);
void func(double);

那么下面的函数调用就会产生二义性错误:

long n = 1000;
func(n);

n 是 long 类型,转换为 int 或 double 的优先级都是一样的,编译器不知道如何抉择。

如果添加一个函数原型void func(long);,或者去掉一个函数原型void func(int);,无论再怎么调用也不会出错了。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门