天天看點

c++ 多态 運作時多态和編譯時多态_Chapter12:多态——從虛函數表到RTTI(二)

c++ 多态 運作時多态和編譯時多态_Chapter12:多态——從虛函數表到RTTI(二)

一、什麼是虛函數表

編譯器之是以能通過指針指向的對象找到虛函數,是因為在建立對象時額外地增加了虛函數表。如果一個類包含了虛函數,那麼在建立該類的對象時就會額外地增加一個數組,數組中的每一個元素都是虛函數的入口位址。不過數組和對象是分開存儲的,為了将對象和數組關聯起來,編譯器還要在對象中安插一個指針,指向數組的起始位置。這個數組就是虛函數表。

假設有三個類,它們之間有繼承關系,寫一下:

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

那麼,每個類的對象模型如下所示:

c++ 多态 運作時多态和編譯時多态_Chapter12:多态——從虛函數表到RTTI(二)

二、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;
}
           

這個例子裡面的虛函數表應該是這樣的:

c++ 多态 運作時多态和編譯時多态_Chapter12:多态——從虛函數表到RTTI(二)

編譯器會在虛函數表 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();
    };
           
c++ 多态 運作時多态和編譯時多态_Chapter12:多态——從虛函數表到RTTI(二)

關于多态的部分到這裡就完結了,撒花~

(如有轉載請注明作者與出處,歡迎建議和讨論,thanks)

繼續閱讀