游戏开发工具

C++RTTI机制精讲(C++运行时类型识别机制)

一般情况下,在编译期间就能确定一个表达式的类型,但是当存在多态时,有些表达式的类型在编译期间就无法确定了,必须等到程序运行后根据实际的环境来确定。下面的例子演示了这种情况:

#include <iostream>
using namespace std;

//基类
class Base
{
public:
    virtual void func();
protected:
    int m_a;
    int m_b;
};
void Base::func(){ cout<<"Base"<<endl; }

//派生类
class Derived: public Base
{
public:
    void func();
private:
    int m_c;
};
void Derived::func(){ cout<<"Derived"<<endl; }

int main()
{
    Base *p;
    int n;
  
    cin>>n;
    if(n <= 100)
    {
        p = new Base();
    }
    else
    {
        p = new Derived();
    }
    cout<<typeid(*p).name()<<endl;

    return 0;
}

输入 45,运行结果为:

45↙
class Base

输入 130,运行结果为:

130↙
class Derived

基类 Base 包含了一个虚函数,派生类 Derived 又定义了一个原型相同的函数遮蔽了它,这就构成了多态。p 是基类的指针,可以指向基类对象,也可以指向派生类对象;*p表示 p 指向的对象。

从代码中可以看出,用户输入的数字不同,*p表示的对象就不同,typeid 获取到的类型也就不同,编译器在编译期间无法预估用户的输入,所以无法确定*p的类型,必须等到程序真的运行了、用户输入完毕了才能确定*p的类型。

根据前面讲过的知识,C++ 的对象内存模型主要包含了以下几个方面的内容:

1、如果没有虚函数也没有虚继承,那么对象内存模型中只有成员变量。

2、如果类包含了虚函数,那么会额外添加一个虚函数表,并在对象内存中插入一个指针,指向这个虚函数表。

3、如果类包含了虚继承,那么会额外添加一个虚基类表,并在对象内存中插入一个指针,指向这个虚基类表。


现在我们要补充的一点是,如果类包含了虚函数,那么该类的对象内存中还会额外增加类型信息,也即 type_info 对象。以上面的代码为例,Base 和 Derived 的对象内存模型如下图所示:

1.jpg


编译器会在虚函数表 vftable 的开头插入一个指针,指向当前类对应的 type_info 对象。当程序在运行阶段获取类型信息时,可以通过对象指针 p 找到虚函数表指针 vfptr,再通过 vfptr 找到 type_info 对象的指针,进而取得类型信息。下面的代码演示了这种转换过程:

**(p->vfptr - 1)

程序运行后,不管 p 指向 Base 类对象还是指向 Derived 类对象,只要执行这条语句就可以取得 type_info 对象。

编译器在编译阶段无法确定 p 指向哪个对象,也就无法获取*p的类型信息,但是编译器可以在编译阶段做好各种准备,这样程序在运行后可以借助这些准备好的数据来获取类型信息。这些准备包括:

1、创建 type_info 对象,并在 vftable 的开头插入一个指针,指向 type_info 对象。

2、将获取类型信息的操作转换成类似**(p->vfptr - 1)这样的语句。


这样做虽然会占用更多的内存,效率也降低了,但这是没办法的事情,编译器实在是无能为力了。

这种在程序运行后确定对象的类型信息的机制称为运行时类型识别(Run-Time Type Identification,RTTI)。在 C++ 中,只有类中包含了虚函数时才会启用 RTTI 机制,其他所有情况都可以在编译阶段确定类型信息。

下面是 RTTI  机制的一个具体应用,可以让代码根据不同的类型进行不同的操作:

#include <iostream>
using namespace std;

//基类
class People
{
public:
    virtual void func(){ }
};

//派生类
class Student: public People{ };

int main()
{
    People *p;
    int n;
  
    cin>>n;
    if(n <= 100)
    {
        p = new People();
    }
    else
    {
        p = new Student();
    }

    //根据不同的类型进行不同的操作
    if(typeid(*p) == typeid(People))
    {
        cout<<"I am human."<<endl;
    }
    else
    {
        cout<<"I am a student."<<endl;
    }

    return 0;
}

可能的运行结果:

83↙
I am human.


多态(Polymorphism)是面向对象编程的一个重要特征,它极大地增加了程序的灵活性,C++、C#、Java 等“正统的”面向对象编程语言都支持多态。但是支持多态的代价也是很大的,有些信息在编译阶段无法确定下来,必须提前做好充足的准备,让程序运行后再执行一段代码获取,这会消耗更多的内存和 CPU 资源。