您好,登錄后才能下訂單哦!
本篇內容介紹了“C++對象模型之RTTI的實現原理是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
RTTI是Runtime Type Identification的縮寫,意思是運行時類型識別。C++引入這個機制是為了讓程序在運行時能根據基類的指針或引用來獲得該指針或引用所指的對象的實際類型。但是現在RTTI的類型識別已經不限于此了,它還能通過typeid操作符識別出所有的基本類型(int,指針等)的變量對應的類型。
C++通過以下的兩個操作提供RTTI:
typeid運算符,該運算符返回其表達式或類型名的實際類型。
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運算符指出操作數的靜態類型,即編譯時的類型。
類型名
一個基本類型的變量
一個具體的對象
一個指向不含有virtual函數的類對象的指針的解引用
一個指向不含有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運算符需要在程序運行時計算類型,因為其其操作數的類型在編譯時期是不能被確定的。
一個指向不含有virtual函數的類對象的指針的解引用
一個指向不含有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函數。多態類的對象的類型信息保存在虛函數表的索引的-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為例。
計算指針或引用變量所指的對象的虛函數表的type_info信息,如下:
*(type_info*)px->vptr[-1]
靜態推導向下轉型的目標類型的type_info信息,即獲取類XX的type_info信息
比較1)和2)中獲取到的type_info信息,若2)中的類型信息與1)中的類型信息相等或是其基類類型,則返回相應的對象或子對象的地址,否則返回NULL。
引用的情況與指針稍有不同,失敗時并不是返回NULL,而是拋出一個bad_cast異常,因為引用不能參考NULL。
“C++對象模型之RTTI的實現原理是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。