时间:2014.03.10
地点:基地二楼
------------------------------------------------------------------------------------
一、简述
C++允许编译器在不同类型之间执行隐式转换,和C一样,char可默默转换为int,short可默默转换为double,正因为这种隐式转换,你可以将short类型交给一个期望获得double类型的函数来处理,这和传统的C风格一脉相承。但可怕的是C++还可将int转换为short,以及将double转换为char等,这可能导致信息丢失。而在我们自己设计类的时候,也可以选择提供适当的函数,供编译器拿来做隐式转换。
------------------------------------------------------------------------------------
二、隐式转换方式
1.单变量构造函数:如果构造函数声明了单一参数,或者有多个参数但除第一个参数不做要求外都有默认值可执行隐式转换:
class Name{
public:
Name(const string& s); //可把string转换为Name类型
.....
};
class Rational{
public:
Rational(int numerator0,int denominator=1);
...
};
2.隐式类型转换操作符:类中拥有一个成员函数,该函数为关键词operator后加一个类型名称。不能为该函数指定返回类型,因为返回类型已经表现在函数名称上。例如为让Ration对象能够隐式为double,,可以如下定义Rational类:
class Rational{
public:
......
operator double() const; //将Rational转换为double
};
这样这个函数就会在下面场景应用中被自动调用发生类型转换
Rational r(1,2); //r的值是1/2
double d=0.5*r; //现在r自动转换为double然后执行乘法运算
------------------------------------------------------------------------------------
三、问题
提供这类隐式转换是存在风险的,我们最好不要提供任何类型转换函数,因为在你不打算也未预期的情况下,他们可能会被调用,导致结果不正确而行为又不直观,程序变得难以调试。比如:
假设你的类想表现出一个分数,希望像内建类型一样输出分数对象内容,即
Rational r(1,2);
cout<<r; //会打印出 1/2
如果你忘了为Rarional 写operator<<,按你的思路应该会执行打印不成功,因为没有合适的operator<<可以调用。可是,你的编译器面对上述情况发现不存在operator<<可接受Rational对象时它会想尽办法执行类型转换动作,换在本例中它会调用operator double顺利将Rational隐式转换为double,成功得到调用打印,于是Rational打印出来就是浮点数了而非你想要的分数类型,而不会给出任何提示。当然这里并不会给你的程序造成灾难,但足以显示隐式类型转换的缺点,它们可能导致不可预期的函数调用。
------------------------------------------------------------------------------------
四、解决办法
然而,许多时候,这种类型转换又是必须的,我们的解决办法是以功能对等的另一种函数去取代类型转换操作符,比如这里为了让Rational转换为double,我们不用operator doube() const 成员函数,而是改用ToDouble的成员函数去执行类型转换。
class Rational{
public:
....
double ToDouble() const; //将Rational转换为double
};
这样这种类型转换需显示调用
Rational r(1,2);
cout<<r; //错误,Rational没有operator<<
cout<<r.ToDouble(); //正确
如此,类型就不会再默默调用类型转换函数,一个典型的应用就是我们的string类型,为了从string对象转换为传统的C风格字符串,提供的办法也是一个显示的c_str成员函数来执行转换行为。现在来考虑但变量构造函数造成的隐式转换,考虑一个针对数组结构的模板类,该数组结构允许用户指定索引上下限:
template<typename T>
class Array{
public:
Array(int lowBound,int highBound);
Array(int size);
T& operator[](int index);
...
};
上面第一个构造函数允许客户指定索引范围,是一个双变量无默认值构造函数,不会成为类型转换函数。第二个构造函数允许用户指定数组元素个数,可被用来作为一个类型转换函数,导致有风险的结果。如下,考虑一个对Array<int>对象进行比较动作的函数。
bool operator==(const Array<int>& lhs,
const Array<int>& rhs);
Array<int> a(10);
Array<int> b(10);
...
for(int i=0;i<10;++i)
if(a==b[i]){ //本应该是a[i]=b[i
//do something for when
}
else{
// do something for other
}
上面代码试图将a的每一个元素拿来和b对应的元素做比较,当键入a时意外漏下小标,当然希望编译器能发现这种错误,可它现在却无动于衷。因为它看到operator==函数调用时,这里实参一个为Array<int> 的变量a和一个为int的变量b[i],虽然没有这样的operator==可以被调用,但编译器注意到可以调用Array<int>构造函数就可以将int转换为Array<int>对象,于是它放手执行这种转换,产生类似这样的代码
for(int i=0;i<10;++i)
if(a==static_cast<Array<int>>(b[i]))...
于是循环的每一次迭代都是拿a的每次迭代内容来和一个大小为b[i]的临时数组(其内容未定义)来做比较。这显然不是你想要的,但编译器从不给出任何提示。这样的程序很难调试。然而单变量构造函数很多情景下又是必须的,我们不得不提供,那么如何消除这种隐式转换的隐患呢?
关键词explicit就是为这个问题而生的。用法简单易懂。只要将构造函数声明为explicit即可,这样编译器就不能因隐式类型转换而去调用它们。当然显式转换是允许的。
template<typename T>
class Array{
public:
...
explicit Aarray(int size); //注意explicit的使用
...
};
Array<int> a(10); //正确
Array<int? b(10); //正确,均是显式调用
if(a==b[i])... //错误,无法将int隐式转换为Array<int>
if(a==Array<int>(b[i]))...// 正确,显式行为,但这里无意义
if(a==static_cast< Array<int> >(b[i]))...//正确,显式行为,但这里无意义
if(a==(Array<int>)b[i])... //C式风格的类型转换,正确,但这里无意义
};
------------------------------------------------------------------------------------
五、总结
总之,类型转换函数分为两种,一种是单变量构造函数,它使得其它类型可能偷偷转换为本类型使用,我的的解决办法是让该单变量构造函数explicit一下。还一种形式是转换类型操作符,它使得本类型可能偷偷转换为其它类型使用,解决的办法是我们在类中提供功能相当的成员函数供显式调用,这样虽有些不便,但消除了风险。总之,对于类型转换函数我们要保持警觉。