加入收藏 | 设为首页 | 会员中心 | 我要投稿 揭阳站长网 (https://www.0663zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 站长资讯 > 传媒 > 正文

真实了解:C++对象模型之RTTI的实现原理

发布时间:2020-10-24 08:34:31 所属栏目:传媒 来源:未知
导读:TTI是Runtime Type Identification的缩写,意思是运行时类型识别。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型

TTI是Runtime Type Identification的缩写,意思是运行时类型识别。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型(int,指针等)的变量对应的类型。

C++通过以下的两个操作提供RTTI:

  • typeid运算符,该运算符返回其表达式或类型名的实际类型。
  • dynamic_cast运算符,该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用。

下面分别详细地说明这两个操作的实现方式。

注所有的测试代码的测试环境均为:32位Ubuntu 14.04 g++ 4.8.2,若在不同的环境中进行测试,结果可能有不同。

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

RTTI是Runtime Type Identification的缩写,意思是运行时类型识别。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型(int,指针等)的变量对应的类型。

作者:linux技术栈来源:今日头条|2020-10-23 18:46

RTTI是Runtime Type Identification的缩写,意思是运行时类型识别。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型(int,指针等)的变量对应的类型。

C++通过以下的两个操作提供RTTI:

  • typeid运算符,该运算符返回其表达式或类型名的实际类型。
  • dynamic_cast运算符,该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用。

下面分别详细地说明这两个操作的实现方式。

注所有的测试代码的测试环境均为:32位Ubuntu 14.04 g++ 4.8.2,若在不同的环境中进行测试,结果可能有不同。

1、typeid运算符

typeid运算符,后接一个类型名或一个表达式,该运算符返回一个类型为std::tpeinf的对象的const引用。type_info是std中的一个类,它用于记录与类型相关的信息。类type_info的定义大概如下:


	
  1. class type_info 
  2.     public
  3.         virtual ~type_info(); 
  4.         bool operator==(const type_info&)const; 
  5.         bool operator!=(const type_info&)const; 
  6.         bool before(const type_info&)const; 
  7.         const charname()const; 
  8.     private: 
  9.         type_info(const type_info&); 
  10.         type_info& operator=(const type_info&); 
  11.         
  12.         // data members 
  13. }; 

至于data members部分,不同的编译器会有所不同,但是都必须提供最小量的信息是class的真实名称和在type_info对象之间的某些排序算法(通过before()成员函数提供),以及某些形式的描述器,用来表示显式的类的类型和该类的任何子类型。

从上面的定义也可以看到,type_info提供了两个对象的相等比较操作,但是用户并不能自己定义一个type_info的对象,而只能通过typeid运算符返回一个对象的const引用来使用type_info的对象。因为其只声明了一个构造函数(复制构造函数)且为private,所以编译器不会合成任何的构造函数,而且赋值操作运行符也为private。这两个操作就完全禁止了用户对type_info对象的定义和复制操作,用户只能通过指向type_info的对象的指针或引用来使用该类。

下面说说,typeid对静态类型的表达式和动态类型的表达式的处理和实现。

1)typeid识别静态类型

当typeid中的操作数是如下情况之一时,typeid运算符指出操作数的静态类型,即编译时的类型。

  • 类型名
  • 一个基本类型的变量
  • 一个具体的对象
  • 一个指向不含有virtual函数的类对象的指针的解引用
  • 一个指向不含有virtual函数的类对象的引用

静态类型在程序的运行过程中并不会改变,所以并不需要在程序运行时计算类型,在编译时就能根据操作数的静态类型,推导出其类型信息。例如如下的代码片断,typeid中的操作数均为静态类型:


	
  1. class X  {  ...... // 具有virtual函数 };  
  2. class XX : public X  { ...... // 具有virtual函数};  
  3. class Y  { ...... // 没有virtual函数};  
  4.   
  5. int main() 
  6.     int n = 0; 
  7.     XX xx; 
  8.     Y y; 
  9.     Y *py = &y; 
  10.   
  11.     // int和XX都是类型名 
  12.     cout << typeid(int).name() << endl; 
  13.     cout << typeid(XX).name() << endl; 
  14.     // n为基本变量 
  15.     cout << typeid(n).name() << endl; 
  16.     // xx所属的类虽然存在virtual,但是xx为一个具体的对象 
  17.     cout << typeid(xx).name() << endl; 
  18.     // py为一个指针,属于基本类型 
  19.     cout << typeid(py).name() << endl; 
  20.     // py指向的Y的对象,但是类Y不存在virtual函数 
  21.     cout << typeid(*py).name() << endl; 
  22.     return 0; 

2)typeid识别多态类型

当typeid中的操作数是如下情况之一时,typeid运算符需要在程序运行时计算类型,因为其其操作数的类型在编译时期是不能被确定的。

  • 一个指向不含有virtual函数的类对象的指针的解引用
  • 一个指向不含有virtual函数的类对象的引用

多态的类型是可以在运行过程中被改变的,例如,一个基类的指针,在程序运行的过程中,它可以指向一个基类对象,也可以指向该基类的派生类的对象,而typeid运算符需要在运行过程中识别出该基类指针所指向的对象的实际类型,这就需要typeid运算符在运行过程中计算其指向的对象的实际类型。例如对于以下的类定义:


	
  1. class X 
  2.     public
  3.         X() 
  4.         { 
  5.             mX = 101; 
  6.         } 
  7.         virtual void vfunc() 
  8.         { 
  9.             cout << "X::vfunc()" << endl; 
  10.         } 
  11.     private: 
  12.         int mX; 
  13. }; 
  14. class XX : public X 
  15.     public
  16.         XX(): 
  17.             X() 
  18.         { 
  19.             mXX = 1001; 
  20.         } 
  21.         virtual void vfunc() 
  22.         { 
  23.             cout << "XX::vfunc()" << endl; 
  24.         } 
  25.     private: 
  26.         int mXX; 
  27. }; 

使用如下的代码进行测试:


	
  1. void printTypeInfo(const X *px) 
  2.     cout << "typeid(px) -> " << typeid(px).name() << endl; 
  3.     cout << "typeid(*px) -> " << typeid(*px).name() << endl; 
  4. int main() 
  5.     X x; 
  6.     XX xx; 
  7.     printTypeInfo(&x); 
  8.     printTypeInfo(&xx); 
  9.     return 0; 

其输出如下:

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

从输出的结果可以看出,无论printTypeInfo函数中指针px指向的对象是基类X的对象,还是指向派生类XX的对象,typeid运行返回的px的类型信息都是相同的,因为px为一个静态类型,其类型名均为PX1X。但是typeid运算符却能正确地计算出了px指向的对象的实际类型。(注:由于C++为了保证每一个类在程序中都有一个独一无二的类名,所以会对类名通过一定的规则进行改写,所以在这里显示的类名跟我们定义的有一些不一样,如类XX的类名,被改写成了2XX。)

那么问题来了,typeid是如何计算这个类型信息的呢?下面将重点说明这个问题。

多态类型是通过在类中声明一个或多个virtual函数来区分的。因为在C++中,一个具备多态性质的类,正是内含直接声明或继承而来的virtual函数。多态类的对象的类型信息保存在虚函数表的索引的-1的项中,该项是一个type_info对象的地址,该type_info对象保存着该对象对应的类型信息,每个类都对应着一个type_info对象。下面就对这一说法进行验证。

使用如以的代码,对上述的类X和类XX的对象的内存布局进行测试:


	
  1. typedef void (*FuncPtr)(); 
  2. int main() 
  3.     XX xx; 
  4.     FuncPtr func; 
  5.     char *p = (char*)&xx; 
  6.     // 获得虚函数表的地址 
  7.     int **vtbl = (int**)*(int**)p; 
  8.     // 输出虚函数表的地址,即vptr的值 
  9.     cout << vtbl << endl; 
  10.     // 获得type_info对象的指针,并调用其name成员函数 
  11.     cout << "t[-1]: " << (vtbl[-1]) << " -> " 
  12.         << ((type_info*)(vtbl[-1]))->name() << endl; 
  13.     // 调用第一个virtual函数 
  14.     cout << "t[0]: " << vtbl[0] << " -> "
  15.     func = (FuncPtr)vtbl[0]; 
  16.     func(); 
  17.     // 输出基类的成员变量的值 
  18.     p += sizeof(int**); 
  19.     cout << *(int*)p << endl; 
  20.     // 输出派生类的成员变量的值 
  21.     p += sizeof(int); 
  22.     cout << *(int*)p << endl; 
  23.     return 0; 

测试代码,对类XX的对象的内存布局进行测试,其输出结果如下:

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

从运行结果可以看到,利用虚函数表的-1的项的地址转换成一个type_info的指针类型,并调用name成员函数的输出为2XX,其输出与前面的测试代码中利用typeid的输出一致。从而可以知道,关于多态类型的计算是通过基类指针或引用指向的对象(子对象)的虚函数表获得的。

从运行的结果可以知道,类XX的对象的内存布局如下:

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

对于以下的代码片断:


	
  1. typeid(*px).name() 

可能被转换成如下的C++伪代码,用于计算实际对象的类型:


	
  1. (*(type_info*)px->vptr[-1]).name(); 

在多重继承和虚拟继承的情况下,一个类有n(n>1)个虚函数表,该类的对象也有n个vptr,分别指向这些虚函数表,但是一个类的所有的虚函数表的索引为-1的项的值(type_info对象的地址)都是相等的,即它们都指向同一个type_info对象,这样就实现了无论使用了哪一个基类的指针或引用指向其派生类的对象,都能通过相应的虚函数表获取到相同的type_info对象,从而得到相同的类型信息。

3)typeid的识别错误的情况

从第2)节可以看到,typeid对于多态类型是通过虚函数表来计算的,若一个基类的指针指向了一个派生类,而该派生类并不存在virtual函数会出现什么情况呢?

例如,把第2)节中的X和XX类中的virtual函数全部去掉,改成以下的代码:


	
  1. class X 
  2.     public
  3.         X() 
  4.         { 
  5.             mX = 101; 
  6.         } 
  7.     private: 
  8.         int mX; 
  9. }; 
  10.   
  11. class XX : public X 
  12.     public
  13.         XX(): 
  14.             X() 
  15.         { 
  16.             mXX = 1001; 
  17.         } 
  18.     private: 
  19.         int mXX; 
  20. }; 

测试代码不变,如下:


	
  1. void printTypeInfo(const X *px) 
  2.     cout << "typeid(px) -> " << typeid(px).name() << endl; 
  3.     cout << "typeid(*px) -> " << typeid(*px).name() << endl; 
  4. int main() 
  5.     X x; 
  6.     XX xx; 
  7.   
  8.     printTypeInfo(&x); 
  9.     printTypeInfo(&xx); // 注释1 
  10.   
  11.     return 0; 

其输出如下:

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

从输出的结果可以看到,对于注释1的函数调用,虽然函数中基类(X)的指针px指向一个派生类对象(XX类的对象xx),但是typeid却并不没有像第2)节那样能正确地通过指针px计算出其所指对象的实际类型。

其原因在于类XX和类X都没有一个virtual函数,所以类XX和类X并不表现出多态类的性质。所以对类的指针的解引用符合第1)节中所说的静态类型,所以其类型信息是在编译时就已经确定的,并不需要在程序运行的过程中运行计算,所以其输出的类型均为1X而没有输出1XX。更进一步说,是因为类X和类XX都不存在virtual函数,所以类X和XX都不存在虚函数表,所以也就没有空间存储跟类X和XX类型有关的type_info对象的地址


(编辑:揭阳站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    热点阅读