C++中的RTTI和虚函数 📅 2026/8/7 23:07:32 RTTI: Runtime Type Identification,运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。C通过以下的两个操作提供RTTI1typeid运算符该运算符返回其表达式或类型名的实际类型。2dynamic_cast运算符该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用。首先介绍下static_cast、dynamic_cast、const_cast和reinterpret_cast。static_castnew_type (expression) dynamic_castnew_type (expression) const_castnew_type (expression) reinterpret_castnew_type (expression)static_caststatic_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_castchar(a);//正确将char型数据转换成int型数据 double *c new double; void *d static_castvoid*(c);//正确将double指针转换成void指针 int e 10; const int f static_castconst int(e);//正确将int型数据转换成const int型数据 const int g 20; int *h static_castint*(g);//编译错误static_cast不能转换掉g的const属性const_castconst_cast用于修改类型的const或volatile属性。①常量指针被转化成非常量的指针并且仍然指向原来的对象②常量引用被转换成非常量的引用并且仍然指向原来的对象③const_cast一般用于修改底指针。如const char *p形式。例const int g1 20; int *h1 const_castint*(g1);//去掉const常量const属性 const int g2 20; int h2 const_castint (g2);//去掉const引用const属性 const char *g3 hello; char *h3 const_castchar *(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_castunsigned 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_casttest_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 test3endl; //static_cast和reinterpret_cast的区别主要在于多重继承 C c; c.m_a 123; c.m_b 231; B* b1 reinterpret_castB*(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: 231dynamic_castdynamic_cast操作用于检测运行时如下类型转换1.将一个指向父类的指针转换为一个指向子类的指针2.将一个父类的左引用转换为一个子类的左引用3.针对C11将父类的左/右引用转换为子类的右值引用需要注意dynamic_cast在将父类cast到子类时dynamic_cast将一个基类对象指针或引用cast到继承类指针dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理。这也是dynamic_cast与其他转换不同的地方dynamic_cast涉及运行时类别检查如果绑定到引用或指针的对象不是目标类型的对象则dynamic_cast失败。父类必须要有虚函数,因为dynamic_cast运行时需要检查RTTI信息,只有带虚函数的类运行时才会检查RTTI。需要注意尽量少使用转型操作尤其是dynamic_cast耗时较高会导致性能的下降尽量使用其他方法替代。RTTI和虚函数普通成员函数一个类如果成员函数不是虚函数的话该函数实现存放在代码区定义多个对象的话都到同样的代码区调用该函数而变量则是每新建一个对象都会新建一块内存来存储该变量。调用普通函数时程序根据指针的类型到该类型类所对应的代码区找到所对应的函数即指针的类型决定了普通函数的调用与指向的对象没关系指针为基类类型就调用基类对应的成员函数指针为派生类类型就调用派生类对应的成员函数。换言之在程序执行之前函数的调用就以这种方式固定下来C 中通常称之为静态解析或者静态绑定也叫做早期绑定。底层关键vtable 与 RTTI 结构拥有虚函数的对象头部存虚表指针 vptr指向虚函数表vtable。 在绝大多数编译器GCC/Clang MSVC实现里GCC每个多态类型对应一个typeinfo对象虚表的第一个 slot 存放指向该typeinfo的指针。MSVC虚表附近关联type_info信息。std::type_info就是 RTTI 的载体里面保存类名字、继承关系、基类列表。RTTI 是运行期数据不是靠解析类名字符串比对而是比较type_info实例以及遍历继承图。当执行dynamic_castDerived*(base_ptr)通过对象的vptr拿到本对象实际运行时类型的typeinfo不是指针声明类型是对象真实类型。将真实对象的type_info和目标转换类型Dest的type_info做继承关系校验是否是目标类型本身是否公有继承自目标类型支持向上、向下、交叉转换如果是多重 / 虚继承还要计算内存偏移调整指针地址。校验不通过 → 返回nullptr指针版本重点判断的是指向对象的真实实际类型不是指针 / 引用的静态声明类型。虚函数若增加虚函数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_castCDerived * (pB1); couttypeid(CBasic).name()endl; couttypeid(CDerived).name()endl; couttypeid(*pB1).name()endl; couttypeid(pB1).name()endl; couttypeid(*pB2).name()endl; couttypeid(pB2).name()endl; if(pD1NULL) coutpointer pD1 is NULLendl; else coutpB1 to pD1 cast successed!endl; CDerived *pD2 dynamic_castCDerived * (pB2); if(pD2NULL) coutpointer pD2 is NULLendl; else coutpB2 to pD2 cast successed!endl; CDerived *pD3 new CDerived(10); CBasic *pB dynamic_castCBasic * (pD3); if(pBNULL) coutpointer pB is NULLendl; else coutpD3 to pB cast successed!endl; CDerived *pD4 static_castCDerived * (pB1); if(pD4NULL) cout pointer pD4 is NULLendl; 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