条款27:尽量少做转型动作
C++的设计目标之一就是保证“类型错误”绝不可能发生,与此目标直接冲撞的是转型操作,不论是C++中继承C部分的转型操作还是C++中所特有的4种类型转换操作,这4中类型转换操作可以参看上篇博客补充(8)进行学习,都或多或少与这个目标有些抵触,因此我们需要尽可能地去满足这个目标!
C语言中转型的形式可能如:
(T)expression或者T(expression),其中后者常用于带有explicit构造函数的类参数,称为“旧式类型”;
由于“旧式类型”形式单一,且功能并没有预想中的那么强,C++中优提供了4种新式类型(new_style),具体参看上篇博文。
请记住:任何一种类型转换往往真的令编译器编译出运行期间执行的码,并不是什么都不做,只是高速编译器把某种类型视为另一种类型!
我们先来看看如下的代码:
class Base{...};
class Derived:public Base{...};
Derived d;
Base* pd=&d;
这儿我们使用Base class指针指向Derived class对象,有时候Base* 指向Derived对象时候,不仅应该保存d在内存中的地址,同时还应该保留Base部分占到了Derived对象的多少部分,我们使用一个offset进行表示!上述代码表示,C++中单一独享可能拥有一个以上的地址,C、Java、C#永远不可能发生这种问题,但C++就可以,因为C++坑呀!其实,在C++中一旦使用多重继承的话,这种问题几乎一直存在,见如下代码:
#include <iostream>
#include <string>
using namespace std;
class A{
public:
A(int a) :a(a){
}
virtual ~A(){
cout << "A的析构函数" << endl;
}
private:
int a;
};
class B{
public:
B(int b) :b(b){
}
virtual ~B(){
cout << "B的析构函数" << endl;
}
private:
int b;
};
class AB :public A,public B{
public:
AB(int a, int b) :A(a), B(b){
}
~AB(){
cout << "AB的析构函数" << endl;
}
};
int main(){
AB *ab = new AB(10, 20);
A* a = ab;
B* b = ab;
cout << a << " " << sizeof(*a) << endl;
cout << b << " " << sizeof(*b) << endl;
cout << ab << " " << sizeof(*ab) << endl;
delete ab;
return 0;
}
运行结果如下:
这里我们可以看到A*和B*的指针地址不同,注意这是C++的特性,也可以看做C++的属性雷吧!!!
关于转型另一个趣事是:我们很容易写出某些似是而非的代码,关于转型操作,我们可能会随机设置导致代码貌似没问题,实际有很大问题:
#include <iostream>
#include <string>
using namespace std;
class A{
public:
A(int a) :a(a){
}
virtual void show(){
cout << "A的show()----->" << a << endl;
}
virtual ~A(){
cout << "A的析构函数" << endl;
}
private:
int a;
};
class AA :public A{
public:
AA(int a, int b) :A(a), b(b){
}
virtual void show(){
//A::show();
static_cast<A>(*this).show();
cout<< "<----->" << b << endl;
}
~AA(){
cout << "AA的析构函数" << endl;
}
private:
int b;
};
int main(){
AA* aa = new AA(100, 20);
aa ->show();
A* a = aa;
cout << a << " " << sizeof(*a) << endl;
cout << aa << " " << sizeof(*aa) << endl;
delete a;
return 0;
}
运行结果:
我们可以看到,输出并不是我们所期望的那样,中间多了一个A的析构函数,这是什么情况???哈哈,被坑了吧!既然你选择了C++,那么它对主人的要求可没那么简单,就像一个淘气的小孩,总会捅出篓子,等着你去收拾,这个转型问题就是这样的一种操作,我们调用static_cast对当前对象进行转换,其实转换的是当前对象*this的基类A部分的临时副本上面的show(),当副本结束时候,调用析构函数,如果我们在A::show()中改变了对象成员,而此时这种操作只是简单地改变了副本的值,同时AA::show()还可以改变对象内容,赞成的直观结果是子类的内容已经被修改而基类的内容根本没有改变,这个对象处于一种“伤残”状态!!!
如何解决呢?很简单!
#include <iostream>
#include <string>
using namespace std;
class A{
public:
A(int a) :a(a){
}
virtual void show(){
cout << "A的show()----->" << a << endl;
}
virtual ~A(){
cout << "A的析构函数" << endl;
}
private:
int a;
};
class AA :public A{
public:
AA(int a, int b) :A(a), b(b){
}
virtual void show(){
A::show();
cout<< "<----->" << b << endl;
}
~AA(){
cout << "AA的析构函数" << endl;
}
private:
int b;
};
int main(){
AA* aa = new AA(100, 20);
aa ->show();
A* a = aa;
cout << a << " " << sizeof(*a) << endl;
cout << aa << " " << sizeof(*aa) << endl;
delete a;
return 0;
}
我们使用dynamic_cast转型的时候,通常是因为我们想在一个derived对象身上执行derived class的操作函数(virtual—>多态),但只有一个Base*,我们只能依靠Base*来处理找对象!有两种方式可以避免这个问题!
1)使用容器并在容器中直接存储指向derived class对象的指针(通常为智能指针):
class Window{...};
class SpecialWindow:public Window{
public:
void blink();//注意,这里没有virtual
...
};
typedef std::vector<std::trl::shared_ptr<Window> >VPW;
VPW winPtrs;
...
for(VPW::iterator iter=winPtrs.begin();iter!=winPtrs.end();++iter){
if(SpecialWindow* psw=dynamic_cast<SpecialWindow *>(iter->get()))
psw->blink();
}
转换为:
typedef std::vector<std::trl::shared_ptr<SpecialWindow> >VPSW;
...
for(VPSW::iterator iter=winPtrs.begin();iter!=winPtrs.end();++iter){
(*iter)->blink();
}
2)可以通过base class接口处理“所有可能之各种Window派生类”,那就是在base class内提供virtual函数做你想对各个Windows派生类做的事。
class Window{
public:
virtual void blink(){}
...
};
class SpecialWindow:public Window{
public:
virtual void blink(){...};//注意,这里是virtual函数
...
};
typedef std::vector<std::trl::shared_ptr<Window> >VPW;
VPW winPtrs;
...
for(VPW::iterator iter=winPtrs.begin();
iter!=winPtrs.end();
++iter){
(*iter)->blink();
}
这两种方式在一定程度上都可以提供一种有效的dynamic_cast替代方案,应该学着使用它们!
绝对需要避免一连串的dynamic_cast,这样会使得你的代码又大又慢,我们应该试着用上面两种方式替换这种问题!
总结:
1)如果可以,尽量避免转型,特别是注重效率的代码中避免dynamic_cast,如果有设计需要转型动作,试着朝着不用转型动作的替代方面设计;
2)如果转型是必要的,试着将其隐藏与某个函数之后,让客户调用函数即可;
3)你可使用new_style转型,也不要使用旧式转型,前者很容易分辨,后者则不具有明显特征!