《Effective C++》阅读总结-类型转换

前言

本文涉及以下条款:

  • E116: 尽量少做转型动作
  • M2: 最好使用C++转型操作符
  • M5: 对定制的“类型转换函数”保持警觉

C++中的类型可以分为两大类:1)内置类型 和 2)自定义类型。类型转换涉及到不同内置类型之间,不同自定义类型之间以及内置类型和自定义类型之间的6种相互转换,如下图所示。
casting

内置类型之间

内置类型之间转换的安全性取决于类型所占空间大小。理论上来说,从所占空间更小的类型转换到所占空间相同或者更大的类型是类型安全的,不会有信息损失(浮点类型除外,其由于自身表述的问题不可避免地存在精度损失),相反则会损失部分信息。例如从intdouble,从floatdouble无精度损失,相反存在精度损失,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>

using namespace std;

int main()
{
/*int to double*/
int a = 10;
double b = a;
cout << b << endl; // 10 无精度损失

/*double to int*/
double c = 0.3213;
int d = double(c);
cout << d << endl; // 0 精度损失了(*)

/*float to double*/
float e = 0.0123456789;
double f = e;
cout.precision(10);
cout << e <<endl; // 0.0123456791 float由于精度限制有效位在小数点后第8位
cout << f <<endl; // 0.0123456791 没有精度损失 (*)
/*double to float*/
double g = 0.01234567890123456789;
float h = g;
cout.precision(20);
cout << g <<endl; // 0.012345678901234568431 double由于精度限制有效位在小数点后第17位
cout << h <<endl; // 0.012345679104328155518 精度损失了,第8位开始不一致 (*)
return 0;
}

上例还表明,内置类型之间的相互转换是隐式的,可以由编译器完成。我们也可以用C++类型转换操作符 static_cast完成隐式转换,它的优点在于容易辨识出代码中类型被破坏的地方。

内置类型和自定义类型之间

内置类型通过构造函数转换为自定义类型,而自定义类型可以通过成员函数转换为自定义类型。考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;
class Integer{
private:
int number;
public:
/*constructor*/
Integer(int num):number(num){};

/*member function*/
operator int() const{
return number;
}
};
int main()
{
Integer n = 1;
cout << n << endl; // output: 1
}

示例中,我们声明了一个包装类Integer保存intintInteger的转换是隐式的,由构造函数完成,Integerint的转换也是隐式的,因此我们可以像操作内置类型一样在intInteger之间无缝切换。这种隐式转换的写法给用户带来了极大的便利,但是也让用户的错误难以被发现。

条款M5给出了两个隐式转换可能给用户带来的麻烦。

第一个例子上面的代码已经展示,我们在没有为Integer重载<<操作符的情况下却成功调用了,这归功于编译器为我们调用int()函数将Integer类型转为了int类型,但是有时候这并不是我们想要的,例如我们声明了一个有理数类Rational,它重载了double类型转换操作符,但是我们想让它输出分数形式的有理数,如果我们在操作符<<后面直接跟Rational类型的对象,它会被转换为double类型输出,这不是我们想要的。如果我们不重载类型转换操作符,编译器便会提醒我们没有重载<<操作符,这是我们期待的行为。这个例子实际上解释的并不是很令人信服,我的看法是不重载类型转换操作符,而使用显式接口如asDouble(),asInt()可以提醒类型的使用者,我们在进行类型转换,需要在后续代码中仔细处理。将Integer类的类型转换操作符int改为显示的接口为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;
class Integer{
private:
int number;
public:
/*constructor*/
Integer(int num):number(num){};

/*member function*/
int asInt() const{
return number;
}
};
int main()
{
Integer n = 1;
cout << n.asInt() << endl; // output: 1
}

第二个例子是构造函数引发的隐式转换存在的问题,考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class T>
class Array
{
public:
Array(int size);
...
};

int main(){
Array<int> a,b;
...
for(int i = 0 ; i < 10; i++)
{
if(a == b[i]) // 书写错误,但编译器并不会报错!(*)
...
}
}

我们想遍历两个Array容器中每个元素,但是(*)处漏写了a的下标,我们期待编译器可以发现这一错误,然而并没有,并且这一错误也很难被我们发现。问题出在编译器将整型的变量b[i]通过构造函数转换为了Array<int>类型的对象,然后再将这一临时对象与a进行比较。这显然与我们初衷相去甚远。解决方法是将构造函数用explicit修饰,编译器便不会进行隐式转换,此外也可以用代理类禁止此类行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<class T>
class Array
{
public:
explicit Array(int size); // 通过explicit禁止隐式转换
...
};

int main(){
Array<int> a,b;
...
for(int i = 0 ; i < 10; i++)
{
if(a == b[i]) // 编译器报错!(*)
...
}
}

自定义类型之间

两个没有继承关系的自定义类型之间的转换可以通过构造函数+成员函数进行相互转换,本节主要讨论在同一继承树下两个类型之间的转换。由于子类必然包含基类的信息,因此子类可以通过向上转换 隐式地 安全 转换为基类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

using namespace std;
class Base{
protected:
int a;
public:
Base():a(1){};
virtual void print(){
cout << a << endl;
cout << "parent class" << endl;
}
};
class Derived:public Base{
public:
Derived(){
a = 2;
};
virtual void print(){
cout << "child class" << endl;
}
};
int main()
{
Derived child;
Base pb = child;
pb.print();

Base* pb1 = &child;
pb1->print();

/*
output:
2
parent class
child class
*/
}

基类转换为子类只能通过dynamic_cast操作符完成,条款E27不建议我们使用该操作符,主要原因是该转换非常缓慢。我们可以使用其他方法达到dynamic_cast的相同目的: 1)直接使用子类,而不是基类指针,指向子类对象;2) 在基类中声明需要调用的子类接口,使用虚函数重载该接口。

其他转换

C++提供了四种转型方法,上文中已经给出了static_castdynamic_cast两种类型转换, 这里完整地给出了所有转型方法。

  • static_cast 显式地调用隐式转换,作用在于提醒用户类型转换
  • dynamic_cast 基类转换为子类,应尽量避免使用,而用虚函数或者直接使用子类
  • const_cast 消除变量的常量性
  • reinterpreter_cast 低级转换,例如将int指针转换为int

小结

  • 内置类型的转换是隐式的,在程序中可以使用 static_cast提醒我们类型的转换
  • 内置类型和自定义类型之间通过explicit修饰的构造函数和成员函数接口互相转换
  • 子类转换为父类是隐式的,需要注意虚函数的变化,尽量不要使用dynamic_cast将父类转换为子类,而用虚函数或者直接使用子类代替。