概述
RTTI是运行阶段类型识别的简写(Runtime type identification)。是一种允许在程序运行过程中对对象的类型进行识别的技术,旨在为程序在运行阶段确定对象类型提供一种标准方式。
在C++中,完成RTTI这项任务主要依靠三种组件,它们是:
dynamic_cast
运算符typeid
运算符typeinfo
类
dynamic_cast
运算符提供了一种将基类指针与派生类指针相互转化的方法,可以用来判断对象的类型是否是某个类的派生类。
typeid
运算符可以解析对象的类型,并返回一个其类型对应的typeinfo对象的引用。
typeinfo
类型用于存储类型的特定信息,例如类名。
但是需要注意的是,这里的派生类和基类的概念是为了体现多态,只有在使用多态的过程中,父子类型转换和判断才是有意义,因此对于没有虚函数的类,最好不要使用这个运算符。
即,RTTI只适用于包含虚函数的类。
dynamic_cast
基本格式
使用dynamic_cast运算符可以将基类指针向派生类进行转化,即 向下转型 ,也可以将派生类指针转化为基类指针,即向上转型。它的使用格式如下:
dynamic_cast<目标类型>(表达式)
表达式应该是一个结果为指针的右值表达式,目标类型就是期望将表达式结果转换成的类型。
我们来看一个粟子:
#include<iostream>
using namespace std;
class A{
public:
virtual void f(){cout << "function f in class A" << endl;} //最好包含一个虚函数
};
class B: public A{
public:
virtual void f(){cout << "function f in class B" << endl;}
virtual void show(){cout << "function show in class B" << endl;}
};
class C: public B{
public:
virtual void f(){cout << "function f in class C" << endl;}
virtual void show(){cout << "function show in class C" << endl;}
};
class D{};
(这里仅是示范,各位小伙伴写代码时最好将声明和定义分开来写哈)
int main()
{
A * p1 = new B(); //A类型指针指向B类对象
B * p2 = new B(); //B类型指针指向B类对象
cout << "p1:" << p1 << endl;
cout << "p2:" << p2 << endl;
B* p3 = dynamic_cast<B*>(p1); //下转型
A* p4 = dynamic_cast<A*>(p2); //上转型
cout << "p3:" << p3 << endl;
cout << "p4:" << p4 << endl;
delete p1,p2;
}
运行结果:
可以看到,这些转化都被成功的执行了。
下转型
但是还不能高兴的太早,需要注意的是,dynamic_cast
运算符的主要功能,是回答“是否可以安全的将对象的地址赋给特定类型的指针”。
因此在进行转化过程中,如果遇到了不安全的转化,dynamic_cast
运算符就会返回空指针,或者在编译时期报错。
让我们再来看一些示例:
首先是下转型
int main()
{
/*下转型*/
A * p1 = new A();
A * p2 = new B();
A * p3 = new C();
cout << "p1:" << p1 << endl;
cout << "p2:" << p2 << endl;
cout << "p3:" << p3 << endl;
B * p4 = dynamic_cast<B*>(p1); //将指向基类对象的基类指针转化为派生类指针
B * p5 = dynamic_cast<B*>(p2);
B * p6 = dynamic_cast<B*>(p3);
cout << "p4:" << p4 << endl;
cout << "p5:" << p5 << endl;
cout << "p6:" << p6 << endl;
delete p1,p2,p3;
}
运行结果:
可以看到,这次的运行结果中出现了一个特殊的情况,就是将指向基类对象的基类指针转化为派生类指针时,dynamic_cast
运算符的执行结果是空指针。这代表着这样的转化是不安全的!!!
这点不难解释,因为指向的对象时基类,它并不包含派生类相对于基类额外的函数和成员,因此如果这样的转换成功,那么程序不小心调用了派生类中新定义的函数造成的后果有可能使灾难性的。所以这种转换是不安全的。
上转型
当情况是上转型时:
int main()
{
/*上转型*/
B * p1 = new B();
B * p2 = new C();
cout << "p1:" << p1 << endl;
cout << "p2:" << p2 << endl;
A* p3 = dynamic_cast<A*>(p1);
A* p4 = dynamic_cast<A*>(p2);
cout << "p3:" << p3 << endl;
cout << "p4:" << p4 << endl;
}
运行结果:
这一次,所有的向上转型都没有什么问题。这也是符合常识的,因为使用一个基类的指针指向一个派生类的对象并不会有什么不妥。
所以,总结一下,当dynamic_cast
运算符遇到了不安全的类型转化时会返回空指针。
而上转型总是安全的,下转型却未必,当实际所指向对象类型是要转化类型的本身或是子类时,它是安全的,是其父类时就是不安全的。
使用dynamic_cast进行类型判断
基于这一点,我们可以将一个指向其派生类的指针进行转化,并使用专属于派生类的函数,在上面声明的三个类中,B类新定义了一个A类中没有的虚函数show()
,仅B类即B类派生类C对象可以访问它,使用A类指针是无法访问的。
我们定义一个根据输入生成类对象的工厂方法,并在主函数中使用这个对象,要求当它是B类或是C类时,调用show()
。
A* factory(int k)
{
A* p = nullptr;
switch(k)
{
case 1:p = new A();break;
case 2:p = new B();break;
case 3:p = new C();break;
default:return nullptr;
}
return p;
}
int main()
{
B* pp = nullptr;
A* p = nullptr;
while(true)
{
int k;
cin >> k;
p = factory(k);
if(p == nullptr)
{
break;
}
p->f(); //调用虚函数f
if(pp = dynamic_cast<B*>(p)) //如果被转化为B类指针值不为空
{
pp->show(); //调用函数show
}
delete p;
}
}
运行结果:
可以看到,对于所有的A类对象,仅调用了他们的f()
函数,而所有的B类和C类函数,都成功的调用了其show()
函数
非继承关系的类
对于这个问题,需要考虑到本来dynamic_cast
运算符是为多态而设计的,对于非继承关系的类,多态的条件自然不成立,各种转化也不会是安全的。
因此对于如下代码:
int main()
{
/*无继承关系*/
A * p1 = new A();
D * p2 = new D();
cout << "p1:" << p1 << endl;
cout << "p2:" << p2 << endl;
D* p3 = dynamic_cast<D*>(p1);
A* p4 = dynamic_cast<A*>(p2);
cout << "p3:" << p3 << endl;
cout << "p4:" << p4 << endl;
}
尝试编译运行的结果是编译不通过:
错误信息:
因此编译器阻止了将D类指针转化为A类指针,但是值得注意的是,编译器仅拒绝了后一条语句而并没有拒绝前一条。拒绝的原因是D不满足多态性质。
我们为类D声明一个派生类E,并赋予D类一个虚函数:
class D{
public:
virtual void f(){}
};
class E: public D{
};
此时再进行编译,程序将不再报错。
因此,对于没有体现多态性质的类是不允许作为表达式值参与dynamic_cast
运算的。
运行程序,结果是:
可见,不同继承体系中的类相互转化时不安全的,因此dynamic_cast
运算符的结果都是空指针。
转换引用
在C++中,除了指针可以用以多态以外,引用也经常是一种选择。
int main()
{
A& r1 = *new B();
B& r2 = *new B();
cout << "r1:" << &r1 << endl;
cout << "r2:" << &r2 << endl;
B& r3 = dynamic_cast<B&>(r1);
A& r4 = dynamic_cast<A&>(r2);
cout << "r3:" << &r3 << endl;
cout << "r4:" << &r4 << endl;
delete &r1,&r2;
}
结果如下:
但是同指针不同的是,引用没有返回空指针一说。因此当存在转换类型不安全时,就只能通过异常来表示了。
这里的异常类型是bad_cast
,使用它需要引入头文件typeinfo
:
#include<typeinfo>
...
int main()
{
A& r1 = *new A();
try{
B& r2 = dynamic_cast<B&>(r1);
}catch(bad_cast &){
cout << "类型转化错误!!!" << endl;
}
delete &r1;
}
运行结果:
typeid & typeinfo
事情到了typeid
和typeinfo
这里就变得简单一些了。至少不会像dynamic_cast
那样存在那么多种的情况。
首先,typeid
是一种运算符,他接受两种参数,一种是某类型的对象,另一种是类。它的返回值将是对应类的typeinfo
对象的引用,类的信息都被封装在了typeinfo
当中。
需要注意的是,使用typeinfo
对象,需要引入同名头文件typeinfo
,对就是刚刚bad_cast
引入的头文件
来看一个示例,还是刚刚的那些类,以及工厂,这回我们使用typeid获取对象的typeinfo对象引用,再调用其name()函数来获取类名称。:
//一个工厂函数
A* factory(int k)
{
A* p = nullptr;
switch(k)
{
case 1:p = new A();break;
case 2:p = new B();break;
case 3:p = new C();break;
default:return nullptr;
}
return p;
}
int main()
{
int k;
A * p = nullptr;
while(true)
{
cin >> k;
p = factory(k);
if(p == nullptr)
{
break;
}
cout << "the type name of the p is :" << typeid(*p).name() << endl; //打印类名信息
delete p;
}
}
运行结果:
对于名称,返回的不一定是原名称,但总是具有辨别性的值。
判断类型相等
另外,typeinfo类重载了==
和!=
,可以用来判断两个对象的类型是否相同
int main()
{
int k;
A * p = nullptr;
while(true)
{
cin >> k;
p = factory(k);
if(p == nullptr)
{
break;
}
if(typeid(*p) == typeid(A)) //比较*p的typeinfo对象是否与A类typeinfo对象相同
{
cout << "this is an object of class A" << endl;
}
if(typeid(*p) != typeid(A)) //比较*p的typeinfo对象是否与A类typeinfo对象不同
{
cout << "this is not an object of class A" << endl;
}
delete p;
}
}
运行结果:
最后,需要注意的事情有:
- typeid翻译的是对象和引用的类型,不能翻译指针指向的类型(毕竟指向某个类型的指针本身也是一种类型)
- typeinfo类对象不能自行定义,仅能使用获得的对象引用。
指针的type名称
补充一个指针在typeinfo中的表示;
int main()
{
int k;
int* pi = &k;
int** ppi = π
A a;
A* pa = &a;
A** ppa = &pa;
cout << typeid(k).name() << endl;
cout << typeid(pi).name() << endl;
cout << typeid(ppi).name() << endl;
cout << typeid(a).name() << endl;
cout << typeid(pa).name() << endl;
cout << typeid(ppa).name() << endl;
}
结果:
看完文章,来关注博主一起学习鸭~~~~
啃书系列往期博客
语言基础部分:
- 啃书《C++ Primer Plus》之 C++ 函数指针
- 啃书《C++ Primer Plus》之 C++ 名称空间1
- 啃书《C++ Primer Plus》之 C++ 名称空间2
- 啃书《C++ Primer Plus》之 C++ 引用
- 啃书《C++ Primer Plus》之 const修饰符修饰 类对象 指针 变量 函数 引用
- 啃书《C++ Primer Plus》之 枚举 内容大全
- 啃书《C++ Primer Plus》 动态内存管理(上) new和delete的使用
面向对象部分:
- 啃书《C++ Primer Plus》 面向对象部分 构造函数基础及其使用 ——初始化列表 构造函数重载与调用 创建对象
- 啃书《C++ Primer Plus》 面向对象部分 类型转换——转换构造函数 与 转换函数
- 啃书《C++ Primer Plus》 面向对象部分 析构函数
- 啃书《C++ Primer Plus》 面向对象部分 深拷贝与浅拷贝问题 拷贝构造函数 赋值函数
- 啃书《C++ Primer Plus》 面向对象部分 动态内存管理(中) 动态对象的创建 重载new和delete
- 啃书《C++ Primer Plus》 面向对象部分 动态内存管理(下) 动态成员管理
- 啃书《C++ Primer Plus》 面向对象部分 静态联编与动态联编
- 啃书《C++ Primer Plus》 面向对象部分 虚机制——虚函数表、虚指针
- 啃书《C++ Primer Plus》 面向对象部分 友元 ——友元函数、友元类、友元成员函数
- 啃书《C++ Primer Plus》 面向对象部分 嵌套类