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

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

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

(1)typeid运算符,该运算符返回其表达式或类型名的实际类型。

(2)dynamic_cast运算符,该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用。

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

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

1、typeid运算符

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

class type_info
{
    public:
        virtual ~type_info();
        bool operator==(const type_info&)const;
        bool operator!=(const type_info&)const;
        bool before(const type_info&)const;
        const char* name()const;
    private:
        type_info(const type_info&);
        type_info& operator=(const type_info&);

        // data members
};

至于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运算符指出操作数的静态类型,即编译时的类型。

(1)类型名

(2)一个基本类型的变量

(3)一个具体的对象

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

(5)一个指向不含有virtual函数的类对象的引用

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

class X  {  ...... // 具有virtual函数 };
class XX : public X  { ...... // 具有virtual函数};
class Y  { ...... // 没有virtual函数}; 

int main()
{
    int n = 0;
    XX xx;
    Y y;
    Y *py = &y;

    // int和XX都是类型名
    cout << typeid(int).name() << endl;
    cout << typeid(XX).name() << endl;
    // n为基本变量
    cout << typeid(n).name() << endl;
    // xx所属的类虽然存在virtual,但是xx为一个具体的对象
    cout << typeid(xx).name() << endl;
    // py为一个指针,属于基本类型
    cout << typeid(py).name() << endl;
    // py指向的Y的对象,但是类Y不存在virtual函数
    cout << typeid(*py).name() << endl;
    return 0;
}

2)typeid识别多态类型

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

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

(2)一个指向不含有virtual函数的类对象的引用

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

class X
{
    public:
        X()
        {
            mX = 101;
        }
        virtual void vfunc()
        {
            cout << "X::vfunc()" << endl;
        }
    private:
        int mX;
};
class XX : public X
{
    public:
        XX():
            X()
        {
            mXX = 1001;
        }
        virtual void vfunc()
        {
            cout << "XX::vfunc()" << endl;
        }
    private:
        int mXX;
};

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

void printTypeInfo(const X *px)
{
    cout << "typeid(px) -> " << typeid(px).name() << endl;
    cout << "typeid(*px) -> " << typeid(*px).name() << endl;
}
int main()
{
    X x;
    XX xx;
    printTypeInfo(&x);
    printTypeInfo(&xx);
    return 0;
}

其输出如下:

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

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

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

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

typedef void (*FuncPtr)();
int main()
{
    XX xx;
    FuncPtr func;
    char *p = (char*)&xx;
    // 获得虚函数表的地址
    int **vtbl = (int**)*(int**)p;
    // 输出虚函数表的地址,即vptr的值
    cout << vtbl << endl;
    // 获得type_info对象的指针,并调用其name成员函数
    cout << "\t[-1]: " << (vtbl[-1]) << " -> "
        << ((type_info*)(vtbl[-1]))->name() << endl;
    // 调用第一个virtual函数
    cout << "\t[0]: " << vtbl[0] << " -> ";
    func = (FuncPtr)vtbl[0];
    func();
    // 输出基类的成员变量的值
    p += sizeof(int**);
    cout << *(int*)p << endl;
    // 输出派生类的成员变量的值
    p += sizeof(int);
    cout << *(int*)p << endl;
    return 0;
}

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

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

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

对于以下的代码片断:

typeid(*px).name()

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

(*(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函数全部去掉,改成以下的代码:

class X
{
    public:
        X()
        {
            mX = 101;
        }
    private:
        int mX;
};

class XX : public X
{
    public:
        XX():
            X()
        {
            mXX = 1001;
        }
    private:
        int mXX;
};

测试代码不变,如下:

void printTypeInfo(const X *px)
{
    cout << "typeid(px) -> " << typeid(px).name() << endl;
    cout << "typeid(*px) -> " << typeid(*px).name() << endl;
}
int main()
{
    X x;
    XX xx;

    printTypeInfo(&x);
    printTypeInfo(&xx); // 注释1

    return 0;
}

其输出如下:

从输出的结果可以看到,对于注释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对象的地址。

然而在C++中即使一个类不具有多态的性质,仍然允许把一个派生类的指针赋值给一个基类的指针,所以这个错误比较隐晦。

2、dynamic_cast运算符

把一个基类类型的指针或引用转换至继承架构的末端某一个派生类类型的指针或引用被称为向下转型(downcast)。dynamic_cast运算符的作用是安全而有效地进行向下转型。

把一个派生类的指针或引用转换成其基类的指针或引用总是安全的,因为通过分析对象的内存布局可以知道,派生类的对象中必然存在基类的子对象,所以通过基类的指针或引用对派生类对象进行的所有基类的操作都是合法和安全的。而向下转型有潜在的危险性,因为基类的指针可以指向基类对象或其任何派生类的对象,而该对象并不一定是向下转型的类型的对象。所以向下转型遏制了类型系统的作用,转换后对指针或引用的使用可能会引发错误的解释或腐蚀程序内存等错误。

例如对于以下的类定义:

class X
{
    public:
        X()
        {
            mX = 101;
        }
        virtual ~X()
        {
        }
    private:
        int mX;
};

class XX : public X
{
    public:
        XX():
            X()
        {
            mXX = 1001;
        }
        virtual ~XX()
        {
        }
    private:
        int mXX;
};

class YX : public X
{
    public:
        YX()
        {
            mYX = 1002;
        }
        virtual ~YX()
        {
        }
    private:
        int mYX;
};

使用如下的测试代码,其中的类型转换均为向下转型:

int main()
{
    X x;
    XX xx;
    YX yx;

    X *px = &xx;
    cout << px << endl;

    XX *pxx = dynamic_cast<XX*>(px); // 转换1
    cout << pxx << endl;

    YX *pyx = dynamic_cast<YX*>(px); // 转换2
    cout << pyx << endl;

    pyx = (YX*)px; // 转换3
    cout << pyx << endl;

    pyx = static_cast<YX*>(px); // 转换4
    cout << pyx << endl;

    return 0;
}

其运行结果如下:

运行结果分析

px是一个基类(X)的指针,但是它指向了派生类XX的一个对象。在转换1中,转换成功,因为px指向的对象确实为XX的对象。在转换2中,转换失败,因为px指向的对象并不是一个YX对象,此时dymanic_cast返回NULL。转换3为C风格的类型转换而转换4使用的是C++中的静态类型转换,它们均能成功转换,但是这个对象实际上并不是一个YX的对象,所以在转换3和转换4中,若继续通过指针使用该对象必然会导致错误,所以这个转换是不安全的。

从上述的结果可以看出在向下转型中,只有dynamic_case才能实现安全的向下转型。那么dynamic_case是如何实现的呢?有了上面typeid和虚函数表的知识后,这个问题并不难解释了,以转换1为例。

1)计算指针或引用变量所指的对象的虚函数表的type_info信息,如下:

*(type_info*)px->vptr[-1]

2)静态推导向下转型的目标类型的type_info信息,即获取类XX的type_info信息

3)比较1)和2)中获取到的type_info信息,若2)中的类型信息与1)中的类型信息相等或是其基类类型,则返回相应的对象或子对象的地址,否则返回NULL。

引用的情况与指针稍有不同,失败时并不是返回NULL,而是抛出一个bad_cast异常,因为引用不能参考NULL。

时间: 2024-10-14 11:04:31

C++对象模型之RTTI的实现原理的相关文章

运行时类型信息RTTI

我们在写C++代码的时候经常碰到使用dynamic_cast进行类型转换的情况,也都知道经过dynamic_cast的转换更加安全,因为dynamic_cast进行了类型检查. 但是可能很多人不知道dynamic_cast是C++ 运行时类型信息(RTTI)机制链条上的一个节点. RTTI提供了两个操作符和一个类: dynamic_cast typeid type_info 整个RTTI, 作为一个整体,暴露给程序员的就是这三个元素.因此我们关注的焦点也就在它们身上了. 什么是RTTI 在C++

运行时类型识别RTTI

1.RTTI的工作原理 例1. 用Class加载对象示例. package RTTI; public class Candy { static{ System.out.println("Loading Candy in static block."); } public static void main(String[] args) { System.out.println("Loading Candy in main method."); } } package

JAVA RTTI类型信息

RTTI 允许你在程序运行时,发现和使用类型信息. Dynamically bound method :  动态绑定方法. polymorphism: 多态. RTTI的实现原理: Class 对象: 包含了 类的信息.每一个类都有一个 Class 对象.当创建一个类的实例时,jvm会使用 "类加载器"的子系统. ClassLoader:所有的类都是在对其第一次使用时,动态地加载到jvm中的.当程序创建第一个类的静态成员引用时,就会加载这个类. Dynamic Loading: 在ja

高手谈Android NDK C++ RTTI 分析

本文意在说明Android NDK 在实现C++ RTTI时的相关数据结构,并从汇编角度分析其内存布局,以帮助理解RTTI的实现原理,同时,分析在逆向过程中如何利用RTTI恢复C++类名信息.        用ndk-build编译C++代码时,默认的C++运行时库(libstdc++)是不支持RTTI的, 需要在Application.mk与Android.mk中进行配置.其它可以选择的C++运行时库有GAbi++.STLport.GNU STL.LLVM libc++, 各种库又分静态链接库

51-C++对象模型分析(下)

继承对象模型 ?  在C++编译器的内部类可以理解为结构体 ?  子类是由父类成员叠加子类新成员得到的 [范例代码]继承对象模型初探 1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 class Demo { 7 protected: 8 int mi; 9 int mj; 10 public: 11 virtual void print() { 12 cout << "

如何成为一名架构师,架构师成长之路(转)

转自http://blog.csdn.net/fei33423/article/details/61934514 如何成为一名架构师,架构师成长之路 原创 2017年03月13日 22:50:34 3116 大量阅读别人的系统实现文章( 架构= 模块图 + 模块流程图(启动 和 主流程 ,可以用拟物 tag) 或者 模块时序图) 动态+静态 .对象很重要,模块很重要. 从产品角度,用户很重要 脑图不需要按空格,收缩行 https://www.processon.com/view/link/58c

《C++ Primer Plus》学习笔记9

<C++ Primer Plus>学习笔记9 第15章 友元.异常和其他 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

C++003基础

1.C++对C的扩展 1简单的C++程序 1.1求圆的周长和面积 数据描写叙述: 半径.周长,面积均用实型数表示 数据处理: 输入半径 r. 计算周长 = 2*π*r : 计算面积 = π* r2 . 输出半径,周长,面积: 方法1:用结构化方法编程,求圆的周长和面积 // count the girth and area of circle #include<iostream.h> using name std; void main () { double r, girth, area ;

C++_RTTI运行阶段类型识别

这部分属于C++的新特性,感觉比较高阶的特性.我把它归于属于奇技淫巧的范畴.了解即可. RTTI是运行阶段类型识别(Runtime Type Identification)的简称. 这是添加到C++中的新特性. 很多老式的编译器不支持它,或者可能包含开关RTTI的编译器设置. RTTI旨在位程序在运行阶段确定对象的类型提供一种标准方式. 很多类库已经为其对象提供了实现这种功能的方式,但是由于C++内部不支持,因此各个厂商的机制通常互不兼容. 创建一种RTTI语言标准将使得未来的库能够彼此兼容.