c++ 多态 运行时多态和编译时多态_Chapter12:多态——从虚函数表到RTTI(二)

62d7e0906052add094ffeb50ffb8ce0c.png

一、什么是虚函数表

编译器之所以能通过指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这个数组就是虚函数表。

假设有三个类,它们之间有继承关系,写一下:

class A {
public:
    virtual void display();
    virtual void func_A();
protected:
    int m_a;
};

class B :public A{
public:
    virtual void display();
    virtual void func_B();
protected:
    int m_b;
};

class C :public A, public B{
public:
    virtual void display();
    virtual void func_C();
protected:
    int m_c;
};

那么,每个类的对象模型如下所示:

de50d5b1fed3bea9776fc9367419e1e4.png

二、RTTI(Runtime Type Identification)——运行时类型识别

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

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

  • 如果没有虚函数也没有虚继承,那么对象内存模型中只有成员变量。
  • 如果类包含了虚函数,那么会额外添加一个虚函数表,并在对象内存中插入一个指针,指向这个虚函数表
  • 如果类包含了虚继承,那么会额外添加一个虚基类表,并在对象内存中插入一个指针,指向这个虚基类表。

现在我们再补充一下,在虚函数表的前面,其实还有一个指向type_info对象的指针,以帮助程序在运行时获取对象的类型信息。那么什么是type_info对象呢?

    class type_info {
    public:
        virtual ~type_info();
        int operator==(const type_info& rhs) const;
        int operator!=(const type_info& rhs) const;
        int before(const type_info& rhs) const;
        const char* name() const;
        const char* raw_name() const;
    private:
        void *_m_data;
        char _m_d_name[1];
        type_info(const type_info& rhs);
        type_info& operator=(const type_info& rhs);
    };
  • const char* name() const:返回一个能表示类型名称的字符串。但是C++标准并没有规定这个字符串是什么形式的,例如对于上面的objInfo.name()语句,VC/VS 下返回“class Base”,但 GCC 下返回“4Base”。
  • bool before (const type_info& rhs) const:判断一个类型是否位于另一个类型的前面,rhs 参数是一个 type_info 对象的引用。但是C++标准并没有规定类型的排列顺序,不同的编译器有不同的排列规则,程序员也可以自定义。要特别注意的是,这个排列顺序和继承顺序没有关系,基类并不一定位于派生类的前面。
  • bool operator== (const type_info& rhs) const:重载运算符“==”,判断两个类型是否相同,rhs 参数是一个 type_info 对象的引用。
  • bool operator!= (const type_info& rhs) const:重载运算符“!=”,判断两个类型是否不同,rhs 参数是一个 type_info 对象的引用。

为了深刻理解RTTI和type_info机制,我们来写一个例子:

#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;
}

这个例子里面的虚函数表应该是这样的:

5195373796b14969b76371d7aad33b7b.png

编译器会在虚函数表 vftable 的开头插入一个指针,指向当前类对应的 type_info 对象。当程序在运行阶段获取类型信息时,可以通过对象指针 p 找到虚函数表指针 vfptr,再通过 vfptr 找到 type_info 对象的指针,进而取得类型信息。虽然这么做会消耗资源,但也是不得已而为之。

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

三、静态绑定、动态绑定与多态

我们知道,函数调用实际上是执行函数体中的代码。函数体是内存中的一个代码段,函数名就表示该代码段的首地址,函数执行时就从这里开始。说得简单一点,就是必须要知道函数的入口地址,才能成功调用函数。找到函数名对应的地址,然后将函数调用处用该地址替换,这称为函数绑定。

在编译期间(包括链接期间)就能找到函数名对应的地址,完成函数的绑定,程序运行后直接使用这个地址即可。这称为静态绑定(Static binding)。但是有时候在编译期间想尽所有办法都不能确定使用哪个函数,必须要等到程序运行后根据具体的环境或者用户操作才能决定。这称为动态绑定(dynamic binding)

在多态的情况下,特别是虚函数,编译器常常不能在编译时期即时知道它指向的类型,所以干脆采用动态绑定。

四、继承链

在 C++ 中,除了 typeid 运算符,dynamic_cast 运算符和异常处理也依赖于 RTTI 机制,并且要能够通过派生类获取基类的信息,或者说要能够判断一个类是否是另一个类的基类,这样上节讲到的内存模型就不够用了,我们必须要在基类和派生类之间再增加一条绳索,把它们连接起来,形成一条通路,让程序在各个对象之间游走。在面向对象的编程语言中,我们称此为继承链(Inheritance Chain)。

下面这个例子看起来简单,但是它的继承模型很复杂:

    class A{
    protected:
        int a1;
    public:
        virtual int A_virt1();
        virtual int A_virt2();
        static void A_static1();
        void A_simple1();
    };
    class B{
    protected:
        int b1;
        int b2;
    public:
        virtual int B_virt1();
        virtual int B_virt2();
    };
    class C: public A, public B{
    protected:
        int c1;
    public:
        virtual int A_virt2();
        virtual int B_virt2();
    };

2656c6f0bed67fe4f9d6243be83acde4.png

关于多态的部分到这里就完结了,撒花~

(如有转载请注明作者与出处,欢迎建议和讨论,thanks)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值