天天看点

C++ RTTI和类型转换运算符

目录

一、RTTI

1、dynamic_cast运算符

  2、dynamic_cast实现原理

3、typeid 运算符和type_info类

4、typeid 实现原理

二、类型转换运算符

1、static_cast 运算符

2、const_cast

3、reinterpret_cast

一、RTTI

     RTTI是Runtime Type Identification的缩写,即运行时类型识别,主要用于运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型,进而调用实际类型的特定方法。C++在编译器层面提供了typeid和dynamic_cast两个运算符来支持RTTI。

1、dynamic_cast运算符

     dynamic_cast不能获取某个基类指针或者引用指向或者引用的实际类型,但是能够判断该基类指针或者引用能否安全的转换为某个实际类型,如果能够转换则返回该实际类型的指针或者引用,如果不能够转换则返回空指针,因为没有空引用所以这种情况下会抛出bad_cast异常。注意使用dynamic_cast要求基类必须提供虚方法,否则直接报错源类型不是多态的。

利用dynamic_cast对基类指针做转换的示例如下:

#include <iostream>

using std::cout;

class ClassA {
public:
	virtual ~ClassA() {
	}
	;
	virtual void say() {
		cout << "ClassA\n";
	}
	;
};

class ClassB: public ClassA {
public:
	void say() {
		cout << "ClassB\n";
	}
	;
	void sayB() {
		cout << "sayB\n";
	}
	;
};

class ClassC: public ClassB {
public:
	void say() {
		cout << "ClassC\n";
	}
	;
	void sayB() {
		cout << "ClassC sayB\n";
	}
	;
	void sayC() {
		cout << "sayC\n";
	}
	;
};

int main() {
	//a的指针类型是ClassA,无法调用a的实际类型ClassC的特定方法
	ClassA * a = new ClassC;
//	ClassA * a = new ClassB;
	//如果ClassB不包含虚函数,则直接报错源类型不是多态的
	ClassB * b = dynamic_cast<ClassB*>(a);
	//如果不能转换,b是空指针,if为false
	if (b) {
		//say是虚方法,依然按照a实际指向的类型ClassC调用其say方法
		b->say();
		//sayB不是虚方法,按照b的指针类型ClassB调用其sayB方法
		b->sayB();
	}
	ClassC * c = dynamic_cast<ClassC*>(a);
	if (c) {
		c->say();
		c->sayC();
	}

	return 0;
}
           

 利用dynamic_cast对基类引用做转换的示例如下:

#include <iostream>
#include <typeinfo>

using std::cout;
using std::bad_cast;

class ClassA {
public:
	virtual ~ClassA() {
	}
	;
	virtual void say() {
		cout << "ClassA\n";
	}
	;
};

class ClassB: public ClassA {
public:
	void say() {
		cout << "ClassB\n";
	}
	;
	void sayB() {
		cout << "sayB\n";
	}
	;
};

class ClassC: public ClassB {
public:
	void say() {
		cout << "ClassC\n";
	}
	;
	void sayB() {
		cout << "ClassC sayB\n";
	}
	;
	void sayC() {
		cout << "sayC\n";
	}
	;
};

int main() {
	//模拟方法调用中的将子类实例传给基类引用参数
	ClassB cb;
	ClassA & a = cb;
	try {
		ClassB & b = dynamic_cast<ClassB &>(a);
		b.say();
		b.sayB();
	} catch (bad_cast & e) {
		cout << "cast error,errmsg->" << e.what() << "\n";
	}
	try {
		ClassC & c = dynamic_cast<ClassC &>(a);
		c.say();
		c.sayC();
	} catch (bad_cast & e) {
		cout << "cast error,errmsg->" << e.what() << "\n";
	}

	return 0;
}
           

  2、dynamic_cast实现原理

       dynamic_cast要求基类必须提供虚函数,因此其实现应该跟虚函数表有关,反汇编第一个测试用例,执行ClassB * b = dynamic_cast<ClassB*>(a);时的汇编代码如下:

 <main()+34>: mov    -0x18(%rbp),%rax   将ClassA指针a的地址拷贝到rax中

 <main()+38>: test   %rax,%rax        对rax中的值判断是否为空,即a是否是空指针

 <main()+41>: jne    0x40098f    <main()+50>  如果不是空指针,则跳转到main+50的代码处

 <main()+43>: mov    $0x0,%eax

 <main()+48>: jmp    0x4009a6    <main()+73>

 <main()+50>: mov    $0x0,%ecx    准备__dynamic_cast调用的四个参数

 <main()+55>: mov    $0x400e30,%edx

 <main()+60>: mov    $0x400e50,%esi

 <main()+65>: mov    %rax,%rdi

 <main()+68>: callq  0x400850    <[email protected]>  调用__dynamic_cast函数

 <main()+73>: mov    %rax,-0x20(%rbp)  将调用结果从rax拷贝到栈帧中

__dynamic_cast函数是底层C++ lib包中提供的实现,由C++ Itanium ABI定义,0x400e30和0x400e50两个是编译期确认的两个全局变量,分别是ClassA 和ClassB的类型信息。其大致实现原理跟虚函数调用一样,在虚函数表索引为-1处通过type_info类保存了变量的实际类型信息,根据该实际类型和目标类型的继承关系判断类型转换是否安全,但是该类型信息无法通过info vbl查看,如下图:

C++ RTTI和类型转换运算符

3、typeid 运算符和type_info类

     typeid运算符的入参可以是一个类型名或一个结果为对象的表达式,对象可以是任意类型,注意判断某个基类指针实际指向的类型时必须对该指针变量解引用。该运算符返回一个类型为type_info的对象的const引用,type_info类在头文件typeinfo中定义,其重载了==和!=运算符,可以借此对类型进行比较,type_info类还有一个name()方法可以返回类型信息,不同编译器厂商的实现不一致。注意typeid的入参为空指针时会抛出bad_typeid异常。

基于上述第二个测试用例,main方法修改如下:

int main() {
	ClassA * a = new ClassC;
//	ClassA * a = new ClassB;
    cout<<"typeid(a).name()-->"<<typeid(*a).name()<<"\n";
	if(typeid(ClassC)==typeid(*a)){
		cout<<"is ClassC,name->"<<typeid(ClassC).name()<<"\n";
	}
	if(typeid(ClassB)==typeid(*a)){
		cout<<"is ClassB,name->"<<typeid(ClassB).name()<<"\n";
	}

	return 0;
}
           

执行结果如下:

C++ RTTI和类型转换运算符

上述示例中ClassA包含了虚函数,typeid准确的识别出了变量a的真实类型,如果是普通的不包含虚函数的ClassA了?将该用例的virtual关键字去掉,执行结果如下:

C++ RTTI和类型转换运算符

 说明typeid同dynamic_cast一样,只有在类包含虚函数的情况下才能正确识别出基类指针或者引用实际指向或者引用的类型,与dynamic_cast不同的是,如果基类没有包含虚函数, typeid不会编译报错,而是返回目标变量在编译期确认的类型信息。

4、typeid 实现原理

     在ClassA是虚函数下反汇编cout<<"typeid(a).name()-->"<<typeid(*a).name()<<"\n";,其中typeid操作符相关汇编代码如下:

cmpq   $0x0,-0x18(%rbp)  判断指针a是否非空,如果是空的则执行main()+111

je     0x400a0c    <main()+111>

mov    -0x18(%rbp),%rax    将指针a的地址拷贝到rax中

mov    (%rax),%rax   将rax中的地址处的后8个字节拷贝到rax中,即获取虚函数表的地址

mov    -0x8(%rax),%rax  将虚函数表地址前8个字节拷贝到rax中,即type_info对象的地址

mov    %rax,%rdi   将type_info 对象的地址拷贝到rdi中,准备函数调用

callq  0x400b24    <std::type_info::name()    const>

     在ClassA是非虚函数下反汇编的代码如下:

C++ RTTI和类型转换运算符

非虚函数下,表示type_info类实例的地址变成一个编译期确认的全局变量了。    

      综上可知编译器在编译时为每个类都生成了一个记录其类型信息相关的全局type_info实例,如果是包含虚函数的类,该实例的地址保存在虚函数表索引为-1处,可通过虚函数表获取类型信息。可以用代码模拟上述行为,如下所示:

int main() {
	using std::type_info;
	typedef void (*FUNC)();
	ClassC a;
	ClassA * b = &a;
	long *vp = (long *) (*(long*) &a);
	const type_info & ti = typeid(*b);
	type_info * f = (type_info *) vp[-1];
	cout << "name1->" << ti.name() << "\n";
	cout << "name2->" << f->name() << "\n";
	cout << "is true:" << (f == &ti) << "\n";
	cout << "end\n";

	return 0;
}
           

执行结果如下:

C++ RTTI和类型转换运算符

参考:C++对象模型之RTTI的实现原理

           C/C++杂记:运行时类型识别(RTTI)与动态类型转换原理

二、类型转换运算符

     C中对指针变量的类型转换没有限制,这可以让C轻松获取或者修改任意数据结构在内存中的数据,非常灵活强大,如上一节中用代码获取虚函数表中的type_info实例,但是这样也导致了内存安全问题。同时因为代码编译层面无法对上述转换做校验也导致了很多潜在的类型转换Bug,这些bug不一定导致程序崩溃,其行为是不可预知的。如下示例:

int main() {
	//在ClassA是虚函数下
//	ClassA * a = new ClassC;
	ClassA * a = new ClassA;
	ClassB * b=(ClassB*)a;
	b->sayB();

	return 0;
}
           

上述代码在理论上应该报错,基类实例无法向下做类型转换,但是编译正常,运行结果也正常,如下:

C++ RTTI和类型转换运算符

sayB方法中未使用类属性,如果使用了获取到的类属性的值就是未知的,有可能是非法内存访问而导致程序崩溃,也可能是其他变量占用的内存获取的值未知。因为类似这种指针变量的强制类型转换在C中是非常普遍的,也是C强大的特性之一,C++作为C的超集,必须兼容这种特性,所以就产生上述问题。

    C++为了在代码编译层面对指针类型转换做校验,尽可能暴露类型转换可能导致的问题,添加了4个类型转换运算符,分别是dynamic_cast,static_cast,const_cast,reinterpret_cast,dynamic_cast在上一节中已经清楚了,再看另外3个的用法,注意这4个都只适用于指针或者引用类型变量。

1、static_cast 运算符

      static_cast的用法同dynamic_cast,dynamic_cast是在运行期检查源类型变量能否转换成目标类型变量,要求源类型必须提供虚函数,并且转换完成后调用虚函数时依然使用变量的实际类型的虚函数实现;static_cast是在编译期检查源类型变量能否转换成目标类型变量,要求源类型与目标类型有继承关系或者是内置的默认类型转换,如double转换成int,不要求源类型提供虚函数,如果不符合要求编译报错,转换完成后调用虚函数时使用目标类型的实现,如下示例:

int main() {
	//在ClassA是虚函数下
	ClassA * a = new ClassC;
//	ClassA * a = new ClassA;
	ClassB * b=static_cast<ClassB*>(a);
	b->sayB();
	//编译不报错
	int * i=(int *)a;
	//编译不报错
//    int * s=static_cast<int *>(a);

	return 0;
}
           

2、const_cast

      const_cast用于去掉原变量的const/volitale限制,注意dynamic_cast/static_cast对const变量做类型转换时目标变量也必须带上const,否则编译报错,如下示例:

int main() {

	const ClassA * ca=new ClassC;
	//两处的const都是必须的,否则编译报错
	const ClassB * cb=dynamic_cast<const ClassB *>(ca);
	//编译报错,转换成const以后不能调用类实例方法,因为这可能修改类属性,只能调用类静态方法
//	cb->sayB();

	const int a = 10;
	//编译报错,要求pt必须是const int *
//	int* pt = &a;
	const int* pt = &a;
	//编译报错,pt对应的变量不可修改
//	*pt=12;
	int* b = const_cast<int*>(pt);
	*b = 20;
	cout << "b = " << *b << endl;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "&a = " << &a << endl;

	//编译报错,int不是指针或者引用类型
//	int b2 = const_cast<int>(a);
//	b2=13;

	return 0;
}
           

执行结果如下:

C++ RTTI和类型转换运算符

有趣的是这里打印a的值并未改变,但是*b的值却改变了,b也确实指向a,反汇编找答案。打印*b的汇编代码如下:

C++ RTTI和类型转换运算符

mov    (%rax),%ebx表示将rax地址指向的内存的后4个字节拷贝到rbx中,即读取变量a的值。

打印变量a的代码如下:

C++ RTTI和类型转换运算符

0xa是十六进制的10,即这里打印a是将a作为一个字符常量处理了,编译完成就写死成10了,而没有从a的内存地址读取实际的值,这应该是编译器自己的优化了。为了规避上述优化,将测试代码调整如下:

int main() {

	cout<<"请输入一个数:"<<endl;
	int input;
	cin>>input;

	const int a = input;
	const int* pt = &a;
	int* b = const_cast<int*>(pt);
	*b = 20;
	cout << "b = " << *b << endl;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "&a = " << &a << endl;

	return 0;
}
           

 执行结果如下:

C++ RTTI和类型转换运算符

这回变量a的值变了,反汇编打印a的代码如下:

C++ RTTI和类型转换运算符

 mov    %ebx,%esi就是将变量a的值拷贝到esi中,因为a是在运行期确认的,所以这里无法将其视为字符常量处理,必须在栈中为其分配内存。

3、reinterpret_cast

    reinterpret_cast做的校验非常有限,例如不能将指针变量强转成4字节的int变量,不能将指向函数的指针转换成指向数字的指针,一般情况下跟C中指针强转的效果一样,因此需要谨慎使用,如下示例:

#include <iostream>

using std::cout;
using std::endl;

class ClassA {
private:
	int a=1;
public:
	void say(){cout<<"ClassA";};
};


int main() {

	ClassA * a = new ClassA;
	int * b = reinterpret_cast<int *>(a);
    cout<<*b<<endl;

    //跟reinterpret_cast效果等价
    int * c=(int *)a;
    cout<<*c<<endl;

    //报错损失精度,因为指针是8字节,int是4字节
    //	int b2 = reinterpret_cast<int>(a);
    long b2=reinterpret_cast<long>(a);
    cout<<b2<<endl;

    ClassA a2;
    //报错类型转换无效
//    int b2 = reinterpret_cast<int>(a2);

	return 0;
}
           

    参考:C++的类型转换运算符总结

继续阅读