RTTI: Runtime Type Identification,运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。
C++通过以下的两个操作提供RTTI:
1)typeid运算符,该运算符返回其表达式或类型名的实际类型。
2)dynamic_cast运算符,该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用。
首先介绍下static_cast、dynamic_cast、const_cast和reinterpret_cast。
static_cast<new_type> (expression)
dynamic_cast<new_type> (expression)
const_cast<new_type> (expression)
reinterpret_cast<new_type> (expression)
static_cast
static_cast相当于传统的C语言里的强制转换,该运算符把expression转换为new_type类型,
用来强迫隐式转换如non-const对象转为const对象,编译时检查,用于非多态的转换,可以转换指针及其他,
但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
①用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。
③把空指针转换成目标类型的空指针。
④把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性
例:
char a = 'a';
int b = static_cast<char>(a);//正确,将char型数据转换成int型数据
double *c = new double;
void *d = static_cast<void*>(c);//正确,将double指针转换成void指针
int e = 10;
const int f = static_cast<const int>(e);//正确,将int型数据转换成const int型数据
const int g = 20;
int *h = static_cast<int*>(&g);//编译错误,static_cast不能转换掉g的const属性
const_cast
const_cast,用于修改类型的const或volatile属性。
①常量指针被转化成非常量的指针,并且仍然指向原来的对象;
②常量引用被转换成非常量的引用,并且仍然指向原来的对象;
③const_cast一般用于修改底指针。如const char *p形式。
例:
const int g1 = 20;
int *h1 = const_cast<int*>(&g1);//去掉const常量const属性
const int g2 = 20;
int &h2 = const_cast<int &>(g2);//去掉const引用const属性
const char *g3 = "hello";
char *h3 = const_cast<char *>(g3);//去掉const指针const属性
reinterpret_cast:
new_type必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编辑器,这也就表示它不可移植。
例:
#include <iostream>
using namespace std;
int output(int p){
cout << p <<endl;
return 0;
}
typedef int (*test_func)(int );//定义函数指针test_func
// Returns a hash code based on an address
unsigned short Hash(void *p) {
unsigned int val = reinterpret_cast<unsigned long>(p);
return (unsigned short)( val ^ (val >> 16));
}
class A {
public:
int m_a;
};
class B {
public:
int m_b;
};
class C: public A, public B {};
int main(){
//测试1
cout << "test1:" << endl;
int p = 10;
test_func fun1 = output;
fun1(p);//正确
//test_func fun2 = reinterpret_cast<test_func>(&p);
//fun2(p);//...处有未经处理的异常: 0xC0000005: Access violation
//测试2
cout << "test2:"<<endl;
int a[20];
for (int i = 0; i < 20; i++)
cout << Hash(a + i) << endl;
//测试3
cout << "test3"<<endl;
//static_cast和reinterpret_cast的区别主要在于多重继承
C c;
c.m_a = 123;
c.m_b = 231;
B* b1 = reinterpret_cast<B*>(&c);
B* b2 = static_cast <B*>(&c);
cout << "c address: ";
cout << &c << endl;
cout << "b1 address: ";
cout << b1 << endl;
cout << "b2 address: ";
cout << b2 << endl;
cout << "b1 m_b val: ";
cout << b1->m_b << endl;
cout << "b2 m_b val: ";
cout << b2->m_b << endl;
return 0;
}
结果:
test1:
10
test2:
25485
25481
25477
25473
25469
25465
25461
25457
25453
25449
25445
25441
25437
25433
25429
25425
25421
25417
25413
25409
test3
c address: 0x7fffdcfdbfc0
b1 address: 0x7fffdcfdbfc0
b2 address: 0x7fffdcfdbfc4
b1 m_b val: 123
b2 m_b val: 231
dynamic_cast
dynamic_cast操作用于检测运行时如下类型转换:
1.将一个指向父类的指针转换为一个指向子类的指针
2.将一个父类的左引用转换为一个子类的左引用
3.针对C++11,将父类的左/右引用转换为子类的右值引用
需要注意dynamic_cast在将父类cast到子类时,dynamic_cast将一个基类对象指针(或引用)cast到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理。这也是dynamic_cast与其他转换不同的地方,dynamic_cast涉及运行时类别检查,如果绑定到引用或指针的对象不是目标类型的对象,则dynamic_cast失败。父类必须要有虚函数,因为dynamic_cast运行时需要检查RTTI信息,只有带虚函数的类运行时才会检查RTTI。
需要注意,尽量少使用转型操作,尤其是dynamic_cast,耗时较高,会导致性能的下降,尽量使用其他方法替代。
RTTI和虚函数
普通成员函数
一个类,如果成员函数不是虚函数的话,该函数实现存放在代码区,定义多个对象的话,都到同样的代码区调用该函数,而变量则是每新建一个对象都会新建一块内存来存储该变量。调用普通函数时,程序根据指针的类型到该类型类所对应的代码区找到所对应的函数,即指针的类型决定了普通函数的调用,与指向的对象没关系,指针为基类类型就调用基类对应的成员函数,指针为派生类类型就调用派生类对应的成员函数。换言之,在程序执行之前,函数的调用就以这种方式固定下来,C++ 中通常称之为静态解析或者静态绑定,也叫做早期绑定。
虚函数
若增加虚函数,sizeof一个类对象,会发现比原来大4个字节。多出来的这4个字节就是实现虚函数的关键----虚函数表指针vptr。这个指针指向一张名为"虚函数表"(vtbl)的表,而表中的数据则为函数指针,存储了虚函数fun_b()具体实现所对应的位置。注意,普通函数、虚函数、虚函数表都是同一个类的所有对象公有的,只有成员变量和虚函数表指针是每个对象私有的,sizeof的值也只包括vptr和var所占内存的大小,并且vptr通常会在对象内存的最起始位置。另外,当类有多个虚函数时,仍然只有一个虚函数表指针vptr(指向一个虚函数表),而此时的虚函数表vtbl中会有多个函数指针,分别指向对应的虚函数实现区域。因此,虚函数实现的过程是:通过对象内存中的虚函数指针vptr找到虚函数表vtbl,再通过vtbl中的函数指针找到对应虚函数的实现区域并进行调用。所以虚函数的调用时由指针所指向内存块的具体类型决定的。
把一个函数声明为基类的虚函数就是告诉编译器,派生于这个基类的任何类,该函数调用都是动态的,把类描述为多态,意味着其至少包含一个虚函数。
需要注意,对象调用虚函数总是静态解析,只有引用或者指针调用虚函数,才会进行动态解析。
多态
介绍完前面,我们可以更明确的理解多态这个概念了,多态是一种极为强大的机制,我们常常事先不能确定处理哪种类型的对象,只能在运行期间确定,使用多态可以轻松解决这个问题。例如画图程序,基类定义了draw函数,但具体画方块或者圆圈取决于派生类,那么就可以用基类指针存储用户创建对象的地址,调用draw函数绘制相应的图形。
例:
#include <iostream>
#include <typeinfo>
using namespace std;
class CBasic
{
public:
virtual void test() {
cout << "CBasic Called" << endl;
}
};
class CDerived : public CBasic
{
public:
void test(){
cout << "CDerived Called" << endl;
}
int x;
CDerived (int x) {
this->x = x;
}
void test1(){
cout << "CDerived1 Called" << endl;
}
};
int main()
{
CBasic cBasic;
CDerived cDerived(10);
CBasic *pB1 = new CBasic;
CBasic *pB2 = new CDerived(10);
cout << "The size of the instance is." << endl;
cout << sizeof(pB1) << endl;
cout << sizeof(pB2) << endl;
CDerived *pD1 = dynamic_cast<CDerived *> (pB1);
cout<<typeid(CBasic).name()<<endl;
cout<<typeid(CDerived).name()<<endl;
cout<<typeid(*pB1).name()<<endl;
cout<<typeid(pB1).name()<<endl;
cout<<typeid(*pB2).name()<<endl;
cout<<typeid(pB2).name()<<endl;
if(pD1==NULL)
cout<<"pointer pD1 is NULL"<<endl;
else
cout<<"pB1 to pD1 cast successed!"<<endl;
CDerived *pD2 = dynamic_cast<CDerived *> (pB2);
if(pD2==NULL)
cout<<"pointer pD2 is NULL"<<endl;
else
cout<<"pB2 to pD2 cast successed!"<<endl;
CDerived *pD3 = new CDerived(10);
CBasic *pB = dynamic_cast<CBasic *> (pD3);
if(pB==NULL)
cout<<"pointer pB is NULL"<<endl;
else
cout<<"pD3 to pB cast successed!"<<endl;
CDerived *pD4 = static_cast<CDerived *> (pB1);
if(pD4==NULL)
cout <<"pointer pD4 is NULL"<<endl;
else
cout<< "pointer pD4 cast successed!"<<endl;
cout << pD2->x << endl;
cout << pD4->x << endl;
pD4->test1();
return 0;
}
结果:
The size of the instance is.
8
8
6CBasic
8CDerived
6CBasic
P6CBasic
8CDerived
P6CBasic
pointer pD1 is NULL
pB2 to pD2 cast successed!
pD3 to pB cast successed!
pointer pD4 cast successed!
10
0
CDerived1 Called