1. 程式人生 > >C++物件模型之RTTI的實現原理

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++物件的記憶體佈局,並說明了多型類的物件的型別資訊儲存在虛擬函式表的索引的-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。

相關推薦

C++物件模型RTTI實現原理

RTTI是Runtime Type Identification的縮寫,意思是執行時型別識別。C++引入這個機制是為了讓程式在執行時能根據基類的指標或引用來獲得該指標或引用所指的物件的實際型別。但是現在RTTI的型別識別已經不限於此了,它還能通過typeid操作符識別出所有的

C++物件模型虛擬函式實現原理

在C++中,多型(polymorphism)的意思是,用基類的指標或者引用,定址出一個派生類物件。而虛擬函式(virtual member function)是多型的基礎,這也是面向物件程式設計迷人之處。現在剛好有時間,就寫一下自己對C++在單一繼承情況下如何實現虛擬函式的

C++物件模型記憶體佈局(3)

轉載地址:https://mp.weixin.qq.com/s/dTyAC2IQ50c9nmQGOC0c2A   經過兩天的摸索,今天終於搞清楚C++物件模型.前兩篇C++物件模型之記憶體佈局(2)C++物件模型之記憶體佈局(1)(請戳我)已經講解了單繼承,多重繼承和多繼承的物件模

C++物件模型記憶體佈局(2)

轉載地址:https://mp.weixin.qq.com/s/UQhTAXIHffN3Now4_utb6g   在C++物件模型之記憶體佈局(1)一文中分別講了無多型和有多型時單繼承的物件記憶體佈局,這篇文章將深入講解多重繼承和多繼承.   多重繼承 &nb

C++物件模型記憶體佈局(1)

轉載地址: https://mp.weixin.qq.com/s/LMJ4Hsa1hmued2egk9uWMQ   如果想學習在linux或者在linux平臺下開發,學習C/或C++是非常好的選擇.俗話說,術業有專攻,學一門技術,就儘量學得深,也可以作為行走江湖,混口飯吃的一項本領

C++物件模型記憶體佈局三(虛繼承)

經過兩天的摸索,今天終於搞清楚C++物件模型.前兩篇已經講解了單繼承,多重繼承和多繼承的物件模型.今天講解菱形繼承,雖然過程艱難,但是收穫豐富. 簡單虛繼承物件模型 首先編寫如下的測試程式: 1

深度探索C++物件模型--物件的差異加上多型之後

class zooAnimal{ public: zooAnimal(); virtual ~zoonAnimal(); virtual void rotate(); protected: int loc; string name; }; class Bear :public zooAn

C++札記】C++物件模型記憶體佈局

對於C++的學習,我看了C++ Primer之後,進階的書為深入理解C++物件模型,這本書講解了C++類在記憶體中是如何佈局以及成員函式是怎麼呼叫,有助於理解C++多型是如何實現的.總之,受益匪淺.     無多型的物件佈局 單個類: 假設有以下一個類的定義:

深入理解C++物件模型建構函式

一、前言     學習C++的同學一般都知道有建構函式這個東西,我相信很多同學的理解就是建構函式是用來初始化類成員的,是的,建構函式的本質確實是這樣的,但很多同學會有以下兩個誤解:         (1)任何class如果沒有定義任何建構函式,編譯器就會幫你自動生成一個;

C++物件模型 Copy Constructor的建構操作

序言: 在計算機當中, 當一個class object的內容作為另一個class object的初始值的時候, 如果class 設計者沒有顯示的宣告Copy Constructor, 那麼編譯器將會自動生成Copy Constructor。 那麼在這裡問題就來了: 1、 在什麼情況下會呼

C++物件模型Default Constructor的建構操作

序言: 為了滿足編譯器的需要, 當類設計者沒有顯示的宣告Default Constructor的時候, 編譯器為滿足編譯程式的需要, 將會按照一定的規則自動生成Default Constructor。 一、編譯器自動合成Default Constructor的四種情況 1、類的成員物件包

深度探索c++物件模型vptr初始化語意學

      接上文,還是這個圖,還是這個繼承關係:     其中它們的構造順序是,是從內到外、從根源到末端。所以對一個PVertex物件來說,它的構造順序是:1、Point   2、Point3d  3、Vertex  4、Vertex3d  5、PVertex,然後書中

深入c++物件模型執行期語意學

1.物件的構造與解構   一般而言,constructor 和 destructor的安插都會如你所預期:       //c++偽碼     {       Point point;       //point.Point::Point();一般而言會被安插在這裡    

從零開始學C++虛擬函式與多型(一):虛擬函式表指標、虛解構函式、object slicing與虛擬函式、C++物件模型

#include <iostream>using namespace std;class CObject {public:     virtual void Serialize()     {         cout << "CObject::Serialize ..." <&

讀《深度探索C++物件模型物件成員的效率

測試平臺:華碩N53S(五年前的老機子) 編譯環境:VS2010 接下來我將會有多個測試,在多個不同環境下的所消耗的時間比較: 五個測試分別為:個別的區域性變數、區域性陣列、struct之的Public、class 之中的inline Get函式、 class之中的inli

C/S模型TCP群聊

cpp etl word client inet_addr accep 應用程序 with value 說明:利用TCP協議和多線程實現群聊功能。一個服務器,多個客戶端(同一個程序多次啟動)。客戶端向服務端發送數據,由服務端進行轉發到其他客戶端。 /服務端 // WSAS

C/S模型UDP協議

lob socket 端口號 add 想要 span ipp 技術分享 get 說明:利用UDP協議,創建一個服務器和一個客戶端。兩者間進行通信。由客戶端進行輸入內容,而服務器將接受的內容進行再一次返回,並顯示在服務端。 // UDP_Seversock.cpp :

C/S模型命名管道

實例 命名 效果 bre turn tchar efault 介紹 容量 說明:利用管道實現服務端與客戶端之間的交互。效果等同於利用socket。 命名管道(NamedPipe)是一種簡單的進程間通信(IPC)機制,是服務器進程和一個或多個客戶進程之間通信的單向或雙向管道。

漫談 C++ 的 內存堆 實現原理

當前 就是 問題 spa 設計 所在 內存 vision 分配 如果我來設計 C++ 的 內存堆 , 我會這樣設計 : 進程 首先會跟 操作系統 要 一塊大內存區域 , 我稱之為 Division , 簡稱 div 。 然後

C++物件模型中的虛擬函式分析

對於虛擬函式,知道它的含義,也能夠描述出來。參照百度百科,也就是“它提供了‘動態繫結’機制”。 可總是感覺有些迷糊,於是敲了一段程式碼出來試驗,一探究竟(程式設計環境是VC6.0)。對比程式碼和結果,一切都不言自明。 現在把程式碼和結果貼上來,作為儲存記錄,同時也歡迎大家提出意見,以臻完善。