类型转化

来源:互联网 发布:外围服务 知乎 编辑:程序博客网 时间:2024/04/28 04:43

以前我们提过,当一个类的非explicit构造函数接受一个实参时,如果你在使用该类的地方填入的是那个实参类型,就会发生隐式转化:通过实参临时创建一个该类的对象,然后把这个对象填入使用该类的地方。其实我们也可以自己定义类型的转化。有人可能觉得这件事情不是很有意义,其实,在某些地方,它的使用往往能起到事半功倍的效果。举一个例子说明,加入我们定义了一个“较小”的整数范围0~255,把它封装成一个类。(在图像处理中,8bit灰度图像的像素就是这个范围的)。对像素的处理肯定是要定义加减乘除,大于小于,大于等于,小于等于,等于,不等之类的操作,这意味我们要为每个操作重载一个运算符。而且还要支持这些运算符的操作数不仅是我们自己的类,也有可能是一般的整数,或者浮点数,这意味着我们还要重载这些运算符。虽然可能难度不大,但是如此繁琐的操作也足以让人抓狂。
那么换一个思路就能解决这个问题:我们定义一个从我们的类类型到int的转化操作,这样的话所有跟类有关的算数运算就会先把类转化成int,然后至于int跟任何数据进行算数、关系运算,是否会发生类型转化等等,就交给编译器了!下面我们看看程序是如何实现的:

class SamllInt{public://构造函数SamllInt (int i = 0 ):val(i){if(i <0 || i > 255)throw out_of_range("bad SmallInt initializer");}//转化函数:实现从本类型到int类型的转化operator int() const {return val;}private:std::size_t val;};

转换函数的基本格式是:operator type();其中type代表了你想转换成的类型,可以使内置的,也可以是自定义的(但是不能是viod或者数组,以及函数类型)。这个函数不指定返回值类型,也没有形参。这个函数必须声明为成员函数,而且因为转换时一般不改变成员的对象,所以通常声明为const。
我们不仅可以定义类类型到内置类型的转化,也可以定义类类型到类类型的转化:

class Integral{public://构造函数Integral(int i = 0):val(i){ }//转化函数:从本类转化成SmallInt类operator SamllInt() const {return val % 256 ;}private:size_t val;};

然后就能使用转化函数了:

int main(){SamllInt si(5);//经过类型转化后就能匹配函数的形参cout<<plus1(si)<<endl;//先将SamllInt转换成int,再使用内置转化来实现int到doublecout<<si-3.7<<endl;Integral intval;//使用Integral到SamllInt类的转化,然后调用默认的复制构造函数SamllInt si1(intval);//错误:没有直接的从Integral到int的转化int i = plus1(intval);return 0;}

注意,int i = plus1(intval);这句代码是错误的,因为虽然Integral 可以转化为SamllInt,SamllInt可以转化为int,但是不存在Integral直接到int的转化。
转化虽好,但也不能乱用。原因主要有两个:一是有时候这样的转化意义并不明确。比如对于一个Stduent类,我们完全没有必要去定义从这个类到的它的成员“学号”、“姓名”,“住址”的转化,因为这些成员很少与内置数据类型发生复杂的关系,比如大量的计算,比较等等。当使用它们时,通过get函数来获取可能更好一些。
第二大的原因就是大量的类型转化函数容易产生错误:转化的错误产生的原因很多,从大的角度分,可以分为由实参的匹配引起的,由重载引起的,以及混合表达引起的。下面对这几种情况分别举几个例子:

先看由实参匹配引起的:

int plus_i(int i){return i+1;}double plus_d(double i){return i+1;}long double plus_ld(long double i){return i +1;}using namespace std;int main(){SmallInt si(5);//使用operator int() const重载cout<<plus_i(si)<<endl;//使用operator double() const重载cout<<plus_d(si)<<endl;//二义性cout<<plus_ld(si)<<endl;return 0;}

让我们仔细分析一下这3次调用。对于第一次,使用的是使用operator int() const重载,这时实参与形参完全匹配。第二次有两种可能:1是使用使用operator double() const重载,实现实参到形参的匹配;2是使用operator int() const重载,将si转化为int,然后利用默认参数转化将int转化为duoble。但是直接匹配比需要转化要好,所以编译器选择了直接匹配。对于第三个函数,不论使用哪种类型转化,之后都要使用内置类型转化,没有一个比另一个更好,所以具有二义性。

实参与形参的匹配也会发生在单形参构造函数产生的隐式转化时。举个例子:

void manip(const SmallInt&){}int main(){double d = 0.0;int i = 0;  long double s = 1;manip(d);manip(i);//二义性manip(s);return 0;}


还有一种情况,就是当两个定义了相互转化时,也可能存在二义性:
比如SmallInt类有一个接受Integral类实参的构造函数;而Integral类定义了转化到SmallInt类的操作。那么假如有一个函数的形参是SmallInt类,而你却填入了Integral的形参时,也会发生错误,因为此时既可以通过SmallInt类的构造函数将Integral转化为SmallInt类,然后调用该函数,也可以通过Integral类的转化函数将Integral类转化为SmallInt的转化函数转化为SmallInt类。
此时我们可以通过显示的指明到底使用哪种情况来避免这个问题。当然,最好是不要定义互相提供隐式转化的类。

再看由函数重载引起的:

     void compute(int);      void compute(double);      void compute(long double); 

假如我们的SmallInt只定义转换为int型的操作,那么判断使用哪个重载很简单:
SmallInt转化为int后,与第一个完全匹配;而对于后两个,还要进过标准转化,所以第一函数是最佳的可执行函数。
而不幸的是,我们的SmallInt还定义了到double的转化,此时,就会出现二义性;可以使用int转化调用第一个函数,也可以使用duoble转化调用第二个函数,此时它们都是完全匹配的。此时可以通过强制类型转化来指定使用哪个转化:

compute(static_cast<int>(si));


这种情况也会发生在构造函数的身上:

假如SmallInt类和Integral的构造函数都接受一个int的实参,而一个函数manip却定义了两个重载版本,接受的参数分别为SmallInt和Integral类。那么假如发生下面的调用:

manip(10);

就会出错,因为此时编译器会调用构造函数将10转化成manip需要的型参类型,而形参既可以是SmallInt类,也可以是Integral类,而这两个类恰好都有一个int型参数的构造函数,所以编译器不知道应该使用哪个。同理,此时可以通过显式构造函数来消除二义性:

manip(SmallInt(10));    manip(Integral(10)); 


最后看混合操作引起:

     ClassX sc;      int iobj = sc + 3; 


假如sc定义了转化到int的操作,而ClassX类也重载了+操作,那么此时编译器就无法决定用哪种方式解决这个问题。由此可见如果定义了转化操作符,有定义了重载操作符,则十分容易引起二义性,所以有几点需要注意:
1.不要定义可以相互转化的类,不论是直接的转化还是通过构造函数。
2.不要定义接受内置算数类的转化。如果定义了,那么不要定义它们的重载版本;也不要定义了个以上的对算数类型的转化。

原创粉丝点击