1:基础
C++规则的设计目标之-- 保证类型错误绝对不可能发生。
理论上,如果程序很干净地通过编译,就表示它并不企图在任何对象身上执行任何不安全、无意义、愚蠢荒谬的操作。
但是,转型(cast)破坏了类型系统(type system),这种可能会导致任何种类的麻烦,而且这些麻烦繁琐度不一。C++中的类型转换不像C、Java或C#中的类型转换风险低,所以我们需要小心谨慎使用类型转换。
C++中的类型转换有三种不同的方式:
// 1: C风格
(T)expression; // 将expression转型为T
// 2: 函数风格
T(expression); // 将expression转型为T
// C风格和函数风格被称为旧式转型
// 3: C++风格
static_cast<T>(expression);
dynamic_cast<T>(expression); // T必须是指向多态类型的指针或引用
const_cast<T>(expression); // T必须是指针或引用
reinterpret_cast<T>(expression); // T必须是指针或引用
下面解析一下C++风格的四种类型转换:
- static_cast: 强制隐式转换,如将非常量对象转换为常量对象,将 int 转换为 double 等等。它也可以用来执行上述多种转换的反向转换,如将 void* 指针转换为 typed 指针,将 基类指针转换为派生类指针,但它无法将常量转换为非常量——这个只有 const_cast 才办得到。
- dynamic_cast: 主要用来执行
安全向下转换(safe down casting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费成本极高的转型动作 - const_cast: 通常用来移除对象的常量性,它也是唯一有此能力的C++风格转型操作符。
- reinterpret_cast:执行低级转换,实际结果可能取决于编译器,这也就表示它不可移植。例如将 int* 转换为 int。这一类型在低级代码以外很少见。
2: 宁可使用 C++ style(新式)转型,不要使用旧式转型
虽然旧式转型仍然合法,但新式的C++风格较受欢迎。我们唯一使用旧式转型的时机是,当调用一个 explicit(explicit 可以有效得防止构造函数的隐式转换带来的错误或者误解) 构造函数将一个对象传递给一个函数时。例如:
class Widget {
public:
explicit Widget(int size);
...
};
void doSomething(const Widget& w);
doSomething(Widget(15)); // 更像构造临时对象,但也可以理解为函数风格类型转换
doSomething(static_cast<Widget>(15)); // C++风格类型转换
二者没有功能上的差异,都会把15转换为临时的Widget对象来传进函数。如果要突出创建对象,用函数风格,如果要突出类型转换,就用C++风格。
许多人认为,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型。这是错的, 任何一个类型转换往往真的令编译器编译出运行期间执行的代码。例如下面这段程序:
int x,y;
double d = static_cast<double>(x)/y;
将 int x 转型为 double 肯定会产生一些代码,因为在大部分计算器体系结构中,int 的底层表述不同于 double 的底层表述。接下来是下面这个例子:
class Base{...};
class Derived :
public Base{...};
Derived d;
Base* pb = &d; //derived*隐式转换为base*
这里我们是用一个基类指针指向派生类对象,但有时候上述的两个指针值并不相同。这种情况下会有个偏移量在运行期被施行于 Derived* 指针身上,用以取得正确的 Base* 指针值。这说明了单一对象(例如一个派生类对象)可能拥有一个以上的地址(例如“以Base* 指向它”时的地址和以“Derived* 指向它”时的地址),其它语言中不可能发生这种事,但C++可能。一旦使用多重继承,这一定会发生,即使在单一继承中也可能发生。
请注意,就算你知道这两个地址间存在一个固定的偏移量,在一个平台上偏移量是这么多,不代表另一个平台上的编译器生成的偏移量也是这么多。
另一件关于转型的有趣事情是:我们容易写出某些似是而非的代码(在其它语言中也许是对的)
例如: 许多应用框架都要求派生类的虚函数代码的第一个动作是先调用基类对应的函数,如下面的例子:
class Window{
public:
virtual void onResize(){...}
...
};
class SpecialWindow : public Window{
public:
virtual void onResize(){
static_cast<Window>(*this).onResize();//想要把*this转换为Window再调用它的虚函数
... //执行SpecialWindow的专属操作
}
};
这段程序将 *this 转型为 Window ,因此也能调用 Window ::onResize()。但它调用的并不是真正的当前对象 *this 上的函数,因为类型转换会生成一个基类部分的本地拷贝,所以我们只是用这份暂时副本调用了 onResize()(调用起哪个对象的成员函数虽然听起来没什么的, 可是成员函数都有一个隐藏的this指针, 因此在不同对象上调用成员函数可能会影响成员要函数操作的数据),然后在 *this 这个类上执行 SpecialWindow 的专属操作。
所以上述代码并没有在当前对象上调用Window ::onResize(), 之后又执行SpecialWindow的专属操作. 他是在当前对象的基类的临时副本上调用Window ::onResize(), 然后执行SpecialWindow的专属操作.
如果临时副本的Window ::onResize()修改了对象的内容(有这个可能性, 因为onResize是个non-const成员函数),然后 SpecialWindow::onResize()也修改对象的内容, 这就会使当前对象进入一种伤残的状态--其base class的修改没有落实, 而derived class的修改被落实了.
解决方法:去掉转型动作
class SpecialWindow : public Window{
public:
virtual void onResize(){
Window::onResize(); // 调用Window::onResize作用于*this身上
...
}
...
};
上面这个例子说明了在你打算用类型转换时,错误就可能来找你了,尤其是对于 dynamic_cast 来说。
3: 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast
在什么时候需要dynamic_cast呢?
当想在认定的derived class调用对应的函数,但是你只有一个指向base的pointer或者是reference,这时只能靠他处理对象。
但是dynamic_cast执行速度可能会非常慢,因为有些类型检测的实现是基于字符串对比。假如你在一个4层深的单继承体系内的某个对象做dynamic_cast,就会导致多达4次的 strcmp 调用,而深度继承或多重继承的成本更高。所以在对类型转换保持小心谨慎的同时,更应该在注重效率的代码中对 dynamic_cast保持小心谨慎。
想要避免使用耗费性能的dynamic_cast,有以下两种方法:
方法1:
使用容器储存直接指向派生类对象的指针(经常是智能指针,见条款13),就消除了需要通过基类接口来操作子类的需求。假设在 Window 和 SpecialWindow 继承体系中只有 SpecialWindow 能闪烁,我们可以把以下代码:
class Window{...};
class SpecialWindow : public Window{
public:
void blink();
...
};
typedef std::vector<std::shared_ptr<Window>> VPW; //智能指针存储基类
VPW winPtrs; //通过基类指针操作子类
...
for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
//如果当前对象是一个派生类,if条件里就不是null,就能调用blink()
if(SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get()))
psw->blink(); //但是条件里用了慢的dynamic_cast
}
代替为:
typedef std::vector<std::shared_ptr<SpecialWindow>> VPSW;//智能指针存储派生类
VPSW winPtrs; //现在直接使用派生类指针
...
for(VPSW::iterator = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
(*iter)->blink();//更好的代码,不用dynamic_cast
}
上述方法用容器存储派生类对象, 不存储基类对象.
缺点就是这种方法需要为每一种derived class都创建一个容器来存储. 可能需要多个容器, 且这些容器都具备类型安全性(type-safe).
方法2:
当你在基类中想做一些事情,那么也就基类中同时声明一份,并且将函数设置为virtual的(但是基类中的虚函数什么都不做)
代码如下:
class Window {
public:
virtual void blink() //条款34告诉你缺省实现代码可能是个馊主意
{
//什么都不做
}
};
class SpecialWindows :public Window {
public:
virtual void blink()
{
//实现相关代码
}
};
int main()
{
// 容器, 内含指针指向所有可能的Window类型
std::vector<std::tr1::shared_ptr<Window> > VPW;
for (auto iter = VPW.begin(); iter != VPW.end(); ++iter)
{
(*iter)->blink();
}
return 0;
}
上面的两种替代的方法: 1:适用类型安全容器
2:将virtual 函数继承体系向上移动 都并非放之四海而皆准, 但是很多情况他们都提供了一种dynamic_cast的替代方案.
另外, 绝对必须避免的一件事就是连串的dynamic_cast 例如:
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
for (VPW::iterator iter = winPtrs.begin(); iter != vinPtrs.end(); ++iter)
{
if (SpecialWindow1 * psw1 = dynamic_cast<SpecialWindow1*>(iter->get())){...}
else if (SpecialWindow2 * psw1 = dynamic_cast<SpecialWindow2*>(iter->get())){...}
else if (SpecialWindow3 * psw1 = dynamic_cast<SpecialWindow3*>(iter->get())){...}
...
}
这样产生出来的代码又大又慢,而且基础不稳,因为每次 Window class 集成体系一有改变,所有这一类代码都必须再次检阅看看是否需要修改。例如一旦加入新的 derived class,或许上述连串判断中需要加入新的条件分支。这样的代码应该总是以某些 “基于 virtual 函数调用” 的东西取而代之。
优良的 C++ 代码很少使用转型,我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何肮脏龌龊的动作的影响。
4: 总结:
-
如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。
-
如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码内。
-
宁可使用 C++ style(新式)转型,不要使用旧式转型。前者很容易辨认出来,而且也有着比较分门别类的职责。
Reference: blog.csdn.net/weixin_4261…