上一篇我们介绍了与动态绑定伴随的upcast类型转换,这是一种符合类型安全的类型转换操作。本篇还将介绍有关downcast的类型转换操作,但在此之前,如果你还不熟悉C++的类型系统,可以参考微软相关的开发文档。因为类型系统与类型转换有关,这是我们在C++中的类型转换的一种方式。
当你打算了解动态类型转换的这个话题,我相信你已经知道C++其他类型转换的方法,例如static_cast<Type>或const_cast<Type>.但是dynamic_cast<Type>仅对C++起作用,在C中无效。
让我们回到上一篇的例子,UML如下图
std::string m_name="经理";
Manager* m=new Manager(m_name);
事实上,m1具备两种类型,它既是Employee类型也是Manager类型,它更像类型列表一样。那么它可以轻松将其转换为Employee类型是非常轻松的。但我们关注的是首先派生类Manager实例隐式转换转换为基类Employee实例e1,然后再次转换回Manager类型的实例。
Employee e1=m;
Manager* m1=e1; //非法操作
当然C++编译器是不允许你这么做的,至于这种错误的做法我们在前文已经说过了。
错误的示例:显式的C风格强制类型转换
Manager* m2=(Manager*)e1;//危险操作
这种做法,C++不会报错但这是非常危险的行为,前文已经分析过会造成错误的内存访问和垃圾数据。
下文我们需要引入一种安全的downcast操作,那么动态类型转换(dynamic_cast)就是本文谈论的主题。
destType* dstObj=dynamic_cast<destType*>(src)
- 如果运行时src和destType所引用的对象,是相同类型,或者存在is-a关系(public继承)则转换成功;否则转换失败。
- dynamic_cast只能用来转换多态类型(即定义了虚函数)的对象的指针或引用。如果操作数是指针,成功则返回目标类型的指针,失败返回nullptr。
- 如果操作数是引用,成功则返回目标类型的引用,失败抛出std::bad_cast异常。
dynamic_cast的“运行时类型的转换匹配”,是通过维护一棵由type_info类型对象作为节点的类型继承关系的树,遍历这棵继承树来确定一个待转换的对象的类型和目标类型之间是否存在is-a关系。type_info类型对象是运行时类型信息的一部分。---摘录自维基百科
那么,是否使用动态转换完全取决于你,但本文只是从一个客观的角度论述使用dynamic_cast<Type>基本运行原理。更重要的是,它不仅像编译时那样进行强制转换,还可以在运行时进行类型转换的评估。因此动态类型会在底层执行一些额外的操作,所以具有一定运行时的开销。而dynamic_cast专用于继承层次结构中的类型转换尤其是类型向下转换(downcast)即从基类类型转换成派生类型。
例如:我们只想将一个Manager对象强制转换回一个Employee对象,那么他们可以从Employee类中派生出来,其实很简单,Manager对象已经具有Employee类型,因此可以隐式完成,无需强制转换。尽管如此,仍然可以使用dynamic_cast<Type>进行upcast操作,但显示有点画蛇添足。
另外还有一种复杂的情况,假设我们有一个Employee实例,并且我们想将其转换为Manager对象,据我们所知,它只是Employee类型的指针,我们无法知道它是否为一个Manager对象。dynamic_cast<destType>可以通过运行时类信息查找将一个Employee实例转换为Manager实例,若转换失败将返回一个nullptr,那么使用此返回来验证我们是否在执行下一步操作。
Manager* m=new Employee() //?
dynamic_cast的适用时机
示例1:基类对象到派生类的转换
- 首先我们创建了一个Manager类的实例m.
- 然后执行了upcast操作后得到一个Employee类的实例副本e1
int main(void){
std::string e_name="职员";
std::string m_name="经理";
Manager *m=new Manager(m_name);
Employee *e1=m;
Manager *m1=dynamic_cast<Manager*>(e1);
std::cout<<"原版Manager实例:sizeof "<<sizeof(*m)<<std::endl;
std::cout<<"副本Manager实例:sizeof "<<sizeof(*m1)<<std::endl;
return 0;
}
- 最后我们通过dynamic_cast如上面例子的示例代码,我们仍然有效地得到一个Manager类实例副本,此次操作是从基类实例到派生类实例的转换,我们叫downcast操作,下图是dynamic_cast前后的虚表的条目是一致的。
反例2:同层次的派生类对象到派生类的转换
int main(void){
std::string e_name="职员";
std::string m_name="经理";
Manager *m=new Manager(m_name);
Supervisor* s=new Supervisor(e_name);
Manager *m1=dynamic_cast<Manager*>(s);
if(m1==nullptr){
std::cout<<"转换失败"<<std::end;
}
return 0;
}
其实很明显,同层次的派生类之间是无法转换的,dynamic_cast返回nullptr指针表示转换失败
那么,从上面的两个示例中,你应该发现一个问题.编译器是如何知道实例e1就能成功转换为Mananager实例?又如何知道我们Supervisor类实例无法转换为Manager类实例?它实际上存储运行时的类型信息,即所谓的RTTI
运行期类型信息(Runtime type information,RTTI)指的是在程序运行时保存其对象的类型信息的行为。某些语言实现仅保留有限的类型信息,例如[继承树]信息,而某些实现会保留较多信息,例如对象的属性及方法信息。
这确实增加了开销。但是RTTI可以确保进行类型转换(包含隐式转换和动态类型转换)之类的操作可以安全地进行。
你需要考虑一些性能上的问题。因为运行时的类型存储有关自身的信息,而不是其他类型的信息,并且大规模的动态转换需要相当的时间消耗。dynamic_cast内部实际上会根据RTTI检测目标操作对象的类型信息和用户期待的类型是否匹配,都必须在运行时随时进行验证的。
若你还记得,第10篇《C++继承中虚表的内存布局》已经简单提过类型信息有关概念。除非你有特殊需求,没必要深究C++底层类继承的实现。
小结:
我们为什么在阐述动态绑定中绕了一大圈的原因,大概你们都应该清楚了。因为在类继承过程中,动态绑定通常会涉及类型转换中upcast操作和downcast操作。而这两种类型转换操作中,会涉及一个成员方法(包含虚成员函数)和属性可见性问题。这些问题,在本篇和上一篇我已经说的很清楚了。