1、简述
基类如何安全的转换成子类类型?可以使用 dynamic_cast 运算符,它在转换成进行安全检查,如果不能转换返回空指针。
dynamic_cast只能用于含有虚函数的类之间的转换,因为只有这样的类才能在运行时动态地获取其实际类型信息。
与static_cast相比,dynamic_cast在运行时进行类型检查,因此更加安全,但也会带来一定的性能开销。
2、原理
2.1 运行时类型信息(RTTI)
运行时类型信息(RTTI,Run-Time Type Identification)是C++中用于在程序运行时识别对象类型的一种机制。
每个多态类型都会有一个对应的type_info对象,其中包含了类型的相关信息,如类型名称和继承关系。
对于含有虚函数的类,每个对象都包含一个指向虚函数表的指针。虚函数表中不仅包含了虚函数的地址,还可能包含了类型的元数据(如类型名称、基类指针等)。
2.2 type_info类
type_info是C++标准库中的一个类,用于在运行时表示类型的信息。它通常包含以下几个关键成员函数:
- name():返回一个表示类型名称的字符串。注意,这个名称是编译器依赖的,不同的编译器可能会返回不同的名称。
- ==和!=:用于比较两个type_info对象是否表示相同的类型。
- before():用于在类型信息的集合中对类型进行排序。
2.3 虚函数表(vtable)
函数其实就是用大括号打包了几行代码块;
函数名是指向这个代码块的指针;
C语言中,函数名很简单,每个函数名对应一个代码块;
C++中重载函数,在编译时会将函数的参数叠加到函数名中,每个重载函数指向不同的代码块;
C++中虚函数,在编译时,还没有确定指向哪个代码块,此时函数名和代码块是动态绑定的。
- 静态绑定(binding):在编译期间就可以完成的函数名绑定。
- 动态绑定(binding):在运行期间才可以完成的函数名绑定。
在C++中,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这个函数地址数组被成为虚函数表,这个表中包含了指向类中所有虚函数的指针,以及类的类型信息(RTTI)。
而这个隐藏成员被成为虚表指针(在32位系统占用4个字节,64位系统占用8个字节)。所以,当一个类存在虚函数,那么它就会多占用一个指针大小的内存。这个指针会存在类对象的最前面。
当基类定义了虚函数,基类对象就会将这些虚函数地址都存放到一个函数地址数组中(虚函数表),然后将这个数组的地址存放到对象的开始位置。
派生类对象的开始位置也会有一个虚表指针,指向一个独立的虚函数表,如果派生类重写(重新定义)了基类的虚函数,则会将重写的函数地址替换掉从基类继承的虚函数地址。
2.4 转换过程
1)首先检查待转换的指针或引用是否有效(非空指针或合法引用)
2)查询对象的type_info来判断是否可以进行安全的转换。
3)通过待转换对象的虚函数表指针(vptr)找到该对象的虚函数表。
4)在虚函数表中查找目标类型的虚函数表指针或类型信息(RTTI)。
5)如果转换成功,dynamic_cast会返回指向目标类型的指针或引用。
6)如果转换失败,对于指针类型的转换,dynamic_cast会返回空指针(nullptr);对于引用类型的转换,则会抛出std::bad_cast异常。
3、向下、向上转换
1) 向下转换
向下转换是有风险的,因为基类指针可能并不指向派生类对象。dynamic_cast在运行时检查转换的安全性,如果转换不合法,则返回空指针(对于指针类型)或抛出std::bad_cast异常(对于引用类型)。
2)向上转换
虽然向上转换通常不需要dynamic_cast,但如果你确实需要使用它,它仍然会进行类型检查,确保转换的安全性。然而,在实际应用中,向上转换通常使用static_cast或简单的赋值操作即可。
4、虚函数表示例
1)C++代码
class CAnimal{
public:
virtual void eat(){
cout << "Animal eat" << endl;
}
virtual void run(){
cout << "Animal run" << endl;
}
};
class CDog : public CAnimal{
public:
virtual void run() override{ // 重写,会使用新的虚函数地址
cout << "Dog run" << endl;
}
virtual void jump(){
cout << "Dog jump" << endl;
}
};
2)生成汇编
_ZTV7CAnimal:
.quad 0
.quad _ZTI7CAnimal
.quad _ZN7CAnimal3eatEv
.quad _ZN7CAnimal3runEv
.weak _ZTI4CDog
_ZTV4CDog:
.quad 0
.quad _ZTI4CDog
.quad _ZN7CAnimal3eatEv
.quad _ZN4CDog3runEv
.quad _ZN4CDog4jumpEv
.weak _ZTV7CAnimal
3)解释
基类 CAnimal 中定义了两个虚函数 eat 和 run,对应汇编的中 _ZN7CAnimal3eatEv 和 _ZN7CAnimal3runEv;
子类 CDog 中继承了 eat,重写了 run,新定义了虚函数 jump,对应汇编中的 _ZN7CAnimal3eatEv、_ZN4CDog3runEv 和 _ZN4CDog4jumpEv;
对比后,可以看出,子类 CDog继续使用基类的eat函数,已重写run函数