类型转换函数
C++编译器能够在两种数据类型之间进行隐式转换,它继承了C语言的转换方法,比如允许将char隐式转换为int、从short隐式转换为double。但是C中很多转换可能会导致类型丢失,它们在C++中依然存在。
你对这些类型转换是无能为力的,因为它们是语言本地的特殊。不过当你增加自己的类型时,你就可以由更多的控制力,因为你能选择是否提供函数让编译器进行隐式类型转换。
有两种函数允许编译器进行这些转换:单参数构造函数、隐式类型转换符
- 单参数构造类型是只只有一个参数可以调用的构造函数。该函数可以是只定义了一个参数,也可以是定义了很多参数但是第一个参数之后的所有参数都有缺省值,可以是所有参数都有缺省值
class Name{
public:
Name(const string& s); // 转换string到Name
};
class Rational{
public:
Rational(int numerator = 0, int denominator = 1); // 转换int到有理数类
};
- 隐式类型转换运算符只是一个样子奇怪的成员函数:operator + 类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字。比如:
class Rational{
public:
operator double() const; //转换Rational到double类型
};
// 在下面这种情况下,这个函数会被自动调用
Rational r(1, 2);
double d = 0.5 * r; // r转换为double,然后做乘法
为什么你不需要定义各种类型转换函数
根本原因:当你不需要使用转换函数,这些函数却会被调用运行。可能导致莫名其妙的结果
(1)比如说对于隐式类型转换运算符:如果你想要打印一个有理数对象(假如Rational类没有定义operator<<):
Rational r(1, 2);
cout << r;
当编译器调用operator<<时,会发现没有这样的函数存在,它会试图找到一个合适的隐式类型转换顺序以使得函数调用正常运行。这时,编译器会发现它们能调用Rational::operator double函数把r转换为double-----这将导致灾难性的结果。
解决方法:不使用语法关键字的等同的函数来替代转换运算符。比如为了把Rational对象转换为double,用asDouble函数替换operator double函数:
class Rational {
public:
double asDouble() const;
};
Rational r(1, 2);
cout << r; //错误, Rational对象没有operator <<
cout << r.asDouble(); // 正确
C++标准库中也是这样做的。比如string转换为char* 是使用成员函数c_str来完成的而非类型转换函数。
(2)通过单参数构造函数进行的隐式类型转换更难消除。而且在很多时候这些情况所导致的问题要基于隐式类型转换符
举个例子:
template<class 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应该是a[i]
.....
}else{
.....
}
}
上面不小心把a[i]写成了a,但是编译器并不会告警。因为它把这个调用看成用Array<int>参数和int参数调用operator==函数,然而没有operator==是这样的参数类型,我们的编译器注意到它能通过调用Array<int>构造函数转换int类型到Array<int>类型,也就是:
for(int i = 0; i < 10; ++i){
if(a == static_cast<Array<int>>(b[i])){
这就非常糟糕了。
解决方法:
- 方法一:使用explicit关键字。explicit是为了解决隐式类型转换而特意引入的关键字,当构造函数使用explicit声明时,编译器会拒绝为了隐式类型转换而调用构造函数。显示类型转换依然合法。
template<class T>
class Array{
public:
explicit Array(int size);
};
Array<int> a(10); //ok
a == b[i]; //error,不能隐式转换
Array<int>(b[i]); //ok, 显示转换
static_cast<Array<int>>(b[i]);
- 方法二:使用代理类
目前的需求是需要用整数变量作为构造函数的参数以确定数组大小,但是同时必须防止从整数类型到临时数组对象的隐式转换。为了达到这个目的,可以建立一个新类ArraySize。这个对象只有一个目的就是表示将要建立数组的大小。你必须修改Array的单参数构造函数,用一个ArraySize对象来代替int:
template<class T>
class Array{
public:
class ArraySize{
public:
ArraySize(int numElements) : theSize(numElements){};
int size() const {return theSize};
private:
int theSize;
};
Array(ArraySize size);
};
这里ArraySize是Array的嵌套类,为了强调它总是与Array一起使用,你也必须声明ArraySize为公有,为了让任何人都能使用它
那么:
Array<int> a(10);
将要求编译器用int参数调用Array<int>构造函数,但是没有这样的构造函数。编译器就去寻找,发现它能从int参数转换成一个临时的ArraySize对象,从而构造一个Array对象。
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应该是a[i]
.....
为了调用operator==函数,编译器要求Array<int>对象在”==“的右侧,但是不存在一个参数为int的单参数构造函数。而且编译器无法把int转换成一个临时的ArraySize对象然后用过这个临时对象建立必须的Array<int>,因为这将调用两个用户定义类型的准换,一个int到ArraySize,一个ArraySize到Array<int>,这种转换顺序是被禁止的,所以一定会产生错误。
ArraySize类是一个更通用技术应用案例,类似ArraySize的类常被称为代理类,因为这样的类的每一个对象都是为了这次是其他对象的工作。ArraySize对象实际上是一个整数类型的替代者,用来在建立Array对象时确定数组大小。代理对象能够帮你更好的控制某些方面的行为,否则你就不能控制这些行为
总结
让编译器进行隐式类型转换所造成的弊端远大于它的好处,所以除非你确实需要,请不要定义类型转换函数