C++中RTTI与dynamic_cast

来源:互联网 发布:游戏视频录制软件 编辑:程序博客网 时间:2024/06/05 00:49

前言
最近看了好些个关于C/C++类的书籍,其中几乎每本都会提到C++的类型强转,然后每本书讲的深浅不一,其中《编写高质量C/C++代码的 150个建议》其中有一段对dynamic_cast的举例说明中出现了陈述错误,可能是作者的失误,将static_cast和dynamic_cast的情况说反了,但是这让原本以为对dynamic_cast还算了解我,瞬间懵逼,难道我原来是记错了? 遂百度求证之,结果证明我还是对的,哈哈,然后再林锐的《高质量C++/C编程指南》第二版一书中,对类型转换进行了更深入的讲解,而且此书中对RTTI的讲解也是让我茅塞顿开,下面我就细细道来!


<一>C++的显示类型转换

这个首先,我觉得我们还是得从类型转换讲起;
(既然有显示那么也是有隐式的,不过这里不对隐式转换赘述;)

1.1>四种类型转换运算符
static_cast
static_cast的转换格式:static_cast (expression)

将expression转换为type-id类型,主要用于非多态类型之间的转换,不
提供运行时的检查来确保转换的安全性。主要在以下几种场合中使用:

1.用于类层次结构中,基类和子类之间指针和引用的转换;
当进行上行转换,也就是把子类的指针或引用转换成父类表示,这种转换是安全的;当进行下行转换,也就是把父类的指针或引用转换成子类表示,这种转换是不安全的,也需要程序员来保证;

2.用于基本数据类型之间的转换,如把int转换成char,把int转换成enum等等,这种转换的安全性需要程序员来保证;

3.把void指针转换成目标类型的指针,是及其不安全的;
注:static_cast不能转换掉expression的const、volatile和__unaligned属性。

dynamic_cast
dynamic_cast的转换格式:dynamic_cast (expression)

将expression转换为type-id类型,type-id必须是类的指针、类的引用或者是void *;如果type-id是指针类型,那么expression也必须是一个指针;如果type-id是一个引用,那么expression也必须是一个引用。

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。在多态类型之间的转换主要使用dynamic_cast,因为类型提供了运行时信息;

《引自百度百科》
const_cast :去除对象的const和volatile属性;
reinterpret_cast:允许将任何指针类型转换为其它的指针类型(慎用!!!)

const_cast和reinterpret_cast 不做赘述~


<二> 细说dynamic_cast
2.1 多态
多态这个真的是C++最难理解的内容之一了;我不敢保证我可以把多态讲清楚,但是这里又不得不提多态,dynamic_cast 只适用于含有虚函数的类之间的转换,所以,只取所需部分进行说明:

正如我们所知道的,多态的实现依赖于虚函数和继承(当然,这里是动态多态,这里不提静态多态,函数多态,宏多态之类的东西),更本质的说就是依赖于虚表指针,一般情况下,虚表指针(_vptr)都位于对象的首地址,指向一个虚表(_table),而虚表中的内容则是虚函数的地址;

利用一个接口多种实现实现多态,对应到软件工程里就是,一个变点,多个变体;有些扯远了;

好了,我们暂时只需要知道这些,因为danamic_cast需要用到这些;
(如果觉得自己忘掉了多态,那就去看书吧!!)

2.2 dynamic_cast实例解说

前面已经对dynamic_cast的概念进行了陈列,相信已经帮你回忆好了,那么,下面我们就要对dynamic_cast进行举例说明,更深一步去了解danamic_cast的实现原理!

我们都知道继承的机制是基类的指针(引用)可以指向派生类的指着或者(引用),反之则编译错误;
那么:

//现在我们有这样两个类class Base{public:    virtual void show(){}};class Derive:public Base{public:    virtual void show(){}};

//测试
情景一:

    Base* bptr = new Derive();    //Derive* dptr = bptr;   //error    Derive* dptr = dynamic_cast<Derive*>(bptr);  //没问题    //dptr != NULL

情景二:

    Base* bptr = new Base();    //Derive* dptr = bptr;  //error编译不过    //编译可以过去,但是我们来看看dptr的值    Derive* dptr = dynamic_cast<Derive*>(bptr);    cout<<"dptr = "<<dptr<<endl; //dptr == 0000000;

2.2.1 从情景一进行分析

我们企图用一个基类的指针去赋值一个派生类的指针,注意,基类指针的初始化是用派生类对象指针初始化的,直接进行赋值的话,编译器会直接报错,但是通过dynamic_cast的强制转化就成功了,且dptr != NULL,这是为什么呢?

2.2.2 再从情景二分析

我们企图用一个基类的指针去赋值一个派生类的指针,注意,基类指针的初始化是用基类对象指针初始化的,直接进行赋值的话,编译器会直接报错,但是通过dynamic_cast的强制转化就成功了,但是dptr == NULL,这是为什么呢?

2.2.3 综合分析

其实,如果你仔细看dynamic_cast的定义的话,肯定明白,这就是dynamic_cast的本身的机制,(有兴趣的同学可以去尝试用static_cast测试一下,看看和dynamic_cast有什么不同)这就是dynamic_cast的奇特之处,它可以办到虚函数不能做到的事,而我先现在就是要去探索这个机制的实现原理,这就引出了本篇文章的重头戏RTTI(运行时类型检测的机制);

在侯捷的《探索C++对象模型》和 林锐的《高质量C++/C编程指南》都对RTTI有比较详细的讲述,讲的内容大致相同;


<三> RTTI(Run-time Type Identification)

RTTI的起源是因为异常处理机制,多余的我就不说了,重点就是因为种原因种种原因,它需要一个运行时检查对象类型的手段,那么RTTI就诞生了!

RTTI就是为每个类型增加一个type_info对象;
如果是虚拟机制(即存在虚函数),则type_info对象的指针就放在了vtable中的第一个槽位;

注意,上面所述的内存分布大致如下。可能因为编译器的不同会有差异!
这里写图片描述

3.1.1

上述的理论都是两本书上那么讲的,但是我在我的编译器上进行测试,虚表的第一个槽位就是第一个虚函数的地址,可能是编译器的优化;我还没有明确的在我编译器上找出type_info的指针;(vs2012)

同时,探索C++对象模型这本书上给出的测试可以看出,type_info对象的地址放在虚表的前一个空间:

Aclass* ptra=new Bclass; int ** ptrvf=(int**)(ptra); RTTICompleteObjectLocator str= *((RTTICompleteObjectLocator*)(*((int*)ptrvf[0]-1)));

另外,文章typeid详解中提到:
ISO C++标准并没有确切定义type_info,它的确切定义编译器相关的,但是标准却规定了其实现必需提供如下四种操作(在之后的章节中我会来分析type_info类文件的源码):

t1 == t2 :如果两个对象t1和t2类型相同,则返回true;否则返回false
t1 != t2 :如果两个对象t1和t2类型不同,则返回true;否则返回false
t.name() : 返回类型的C-style字符串,类型名字用系统相关的方法产生
t1.before(t2) : 返回指出t1是否出现在t2之前的bool值

3.1.2 RTTI 和 dynamic_cast的关系

注:(RTTI和虚函数的动态决议不是一回事)林锐:316页;
为了能够在运行时获取对象类型的type_info信息,C++增加了两个运算符:typeid 和 dynamic_cast 两个运算符;

typeid的作用是返回一个type_info对象的引用,typyid我们在平时的编程中也都用到过,想要详细了解type_info的同学可以去看看typeinfo的内部实现,我们平时想要看一个对象的类型,可以用typeid().name()来查看,如果是非多态对象或者是基本数据类型,就会返回静态类型所对应的type_info对像;

真正奇特的地方在于,对于存在继承关系的对象,也就是为了支持dynamic_cast运算符,RTTI必须维护一棵继承树,来确认是否存在is-a关系,这就联系到了dynamic_cast运算符的使用时,如果存在is-a关系时可以正常转换的原因,而更关键的是,dynamic_cast比较的对象的真正类型,而这个真正的类型就是通过访问 vtable 来获取 type_info的,所以,真正支持dynamic_cast运算符的是RTTI;

通过上面讲的这么一大堆,我们现在就可以重新审视前面的情景一和情景二的具体实现过程了;

3.1.3 情景一,二再分析
情景一:

    Base* bptr = new Derive();    //typeid(*bptr).name() 结果:class Derive    cout<<typeid(*bptr).name()<<endl;    //Derive* dptr = bptr;   //error    Derive* dptr = dynamic_cast<Derive*>(bptr);  //没问题    //typeid(*bptr).name() 结果:class Derive    //dptr != NULL

情景二:

    Base* bptr = new Base();    cout<<typeid(*bptr).name()<<endl; //结果:class Base;    //Derive* dptr = bptr;  //error编译不过    //编译可以过去,但是我们来看看dptr的值    Derive* dptr = dynamic_cast<Derive*>(bptr);    //cout<<typeid(*dptr).name()<<endl;  //程序会挂掉;    cout<<"dptr = "<<dptr<<endl; //dptr == 0000000;

dynamic_cast 通过RTTI访问虚表里的type_info,再根据type_info和Deriver进行继承树的遍历判断是否有is-a关系,若有,则返回Deriver对象的引用或指针,否则返回NULL;

利用typeid可以得到对象的确切类型,注意typeid()里的参数是对象,是对dptr进行了解引用,如果是指针的话,返回的是指针的类型,得不到确切的结果的;

这就很好的解释了dynamic_cast和RTTI的关系,并且了解其实现原理!
其实STL里的value_type也用了相似的原理,以后深入学习之后会进行补充!

注:其中很多不完善的地方,以后继续完善!

附RTTI的百度百科,很详细:
http://baike.baidu.com/link?url=b2M8dwztTkzS1Hm9X0rc5-ek8UKvJb4LjOg10FVkwFbYRKAfSEB1e1SPjlCWjtEI_xhc3VipuZWDPcVz4ZVttK

1 3