C++ RTTI與4種類型轉(zhuǎn)換的深入理解
前言
RTTI 是 Run Time Type Information 的縮寫,從字面上來(lái)理解就是執(zhí)行時(shí)期的類型信息,其重要作用就是動(dòng)態(tài)判別執(zhí)行時(shí)期的類型。
并不是說(shuō)這篇文章是RTTI,和用于RTTI的四種類型轉(zhuǎn)換,而是介紹RTTI,再介紹一下4種類型轉(zhuǎn)換,因?yàn)镽TTI有用到其中一種類型轉(zhuǎn)換,所以相當(dāng)于兩篇文章寫在一起。
實(shí)際上 RTTI 用到的是typeid() 和 dynamic_cast()。
為什么會(huì)有RTTI?
C++是一種靜態(tài)類型語(yǔ)言,其數(shù)據(jù)類型是在編譯期就確定的,不能在運(yùn)行時(shí)更改。然而由于面向?qū)ο蟪绦蛟O(shè)計(jì)中多態(tài)性的要求,C++中的指針或引用(Reference)本身的類型,可能與它實(shí)際代表(指向或引用)的類型并不一致。有時(shí)我們需要將一個(gè)多態(tài)指針轉(zhuǎn)換為其實(shí)際指向?qū)ο蟮念愋停托枰肋\(yùn)行時(shí)的類型信息,這就產(chǎn)生了運(yùn)行時(shí)類型識(shí)別的要求。
實(shí)事求是地講,RTTI是有用的。 但因?yàn)橐恍├碚撋霞胺椒ㄕ撋系脑颍茐牧嗣嫦驅(qū)ο蟮募儩嵭浴?/p>
首先, 它破壞了抽象,使一些本來(lái)不應(yīng)該被使用的方法和屬性被不正確地使用。
其次,因?yàn)檫\(yùn)行時(shí)類型的不確定性,它把程序變得更脆弱。
第三點(diǎn),也是最重要的一點(diǎn),它使程序缺乏擴(kuò)展性。 當(dāng)加入了一個(gè)新的類型時(shí),你也許需要仔細(xì)閱讀你的dynamic_cast的代碼,必要時(shí)改動(dòng)它們,以保證這個(gè)新的類型的加入不會(huì)導(dǎo)致問(wèn)題。 而在這個(gè)過(guò)程中,編譯器將不會(huì)給你任何幫助。
總的來(lái)說(shuō),RTTI 因?yàn)樗姆椒ㄕ撋系囊恍┤秉c(diǎn),它必須被非常謹(jǐn)慎地使用。 今天面向?qū)ο笳Z(yǔ)言的類型系統(tǒng)中的很多東西就是產(chǎn)生于避免RTTI的各種努力。
首先我們來(lái)個(gè)例子感受一下:
#include<iostream> #include<typeinfo> using namespace std; class Base { public: virtual void funcA() { cout << "Base" << endl; } }; class Derived : public Base { public: virtual void funcB() { cout << "Derived" << endl; } }; void funcC(Base* p) { Derived* dp = dynamic_cast<Derived*>(p); if (dp != NULL) { dp->funcB(); } else { p->funcA(); } }; void funcD(Base* p) { Derived* dp = NULL; if (typeid(*p) == typeid(Derived)) { dp = static_cast<Derived*>(p); dp->funcB(); } else { p->funcA(); } } int main(int argc, char const* argv[]) { Base* p = new Derived; cout << typeid(p).name() << endl; cout << typeid(*p).name() << endl; funcD(p); funcC(p); delete p; Base* dp = new Base; funcC(dp); funcD(dp); delete dp; return 0; }
funcC是用dynamic_cast類型轉(zhuǎn)換是否成功來(lái)識(shí)別類型的,dynamic_cast操作符將基類類型對(duì)象的引用或指針轉(zhuǎn)化為同一繼承層次中的其他類型的引用或指針。
- 如果綁定到引用或指針的對(duì)象不是目標(biāo)類型的對(duì)象,則dynamic_cast失敗。
- 如果轉(zhuǎn)換到指針類型的dynamic_cast失敗,則dynamic_cast的結(jié)果是NULL值;
- 如果轉(zhuǎn)換到引用類型的dynamic_cast失敗, 則拋出一個(gè)bad_cast類型的異常
funcD是用typeid判斷基類地址是否一致的辦法來(lái)識(shí)別類型的。
typeid
下面我們具體說(shuō)說(shuō) typeid
typeid是C++的關(guān)鍵字之一, 等同于sizeof這類operator。 typeid 操作符的返回結(jié)果是名為 type_info的標(biāo)準(zhǔn)庫(kù)類型的對(duì)象的引用,在頭文件typeinfo 中定義。 有兩種形式:
- typeid(type)
- typeid(expression)
表達(dá)式的類型是類類型,且至少含有一個(gè)虛函數(shù),則typeid操作符返回表達(dá)式的動(dòng)態(tài)類型,需要在運(yùn)行時(shí)計(jì)算,否則返回表達(dá)式的靜態(tài)類型,在編譯時(shí)就可以計(jì)算。
C++標(biāo)準(zhǔn)規(guī)定了其實(shí)現(xiàn)必須提供如下四種操作:
- t1 == t2: 如果兩個(gè)對(duì)象t1和t2類型相同,則返回true;否則返回false
- t1 != t2: 如果兩個(gè)對(duì)象t1和t2類型不同,則返回true;否則返回false
- t.name(): 返回類型的c-style字符串,類型名字用系統(tǒng)相關(guān)的方法產(chǎn)生
- t1.before(t2): 返回指出t1是否出現(xiàn)在t2之前 的bool值
type_info類提供了public虛析構(gòu)函數(shù),以使用戶能夠用其作為基類。它的默認(rèn)構(gòu)造函數(shù)和復(fù)制構(gòu)造函數(shù)及賦值操作符都定義為private,所以不能定義或復(fù)制type_info 類型的對(duì)象。 程序中創(chuàng)建type_info對(duì)象的唯一方法是使用typeid操作符。 由此可見(jiàn),如果把typeid看作函數(shù)的話,其應(yīng)該是type_info的友元。 這具體由編譯器的實(shí)現(xiàn)所決定,標(biāo)準(zhǔn)只要求實(shí)現(xiàn)為每個(gè)類型返回唯一的字符串。
例1: C++里面的typeid運(yùn)箕符返回值是什么?
答: 常量對(duì)象的引用。
如果p是基類指針,并且指向一個(gè)派生類型的對(duì)象,并且基類中有虛函數(shù),那么typeid(*p)返回p所指向的派生類類型,typeid(p)返回基類類型。
RTTI的實(shí)現(xiàn)原理: 通過(guò)在虛表中放一個(gè)額外的指針,每個(gè)新類只產(chǎn)生一個(gè)typeinfo實(shí)例,額外指針指向typeinfo, typeid返回對(duì)它的一個(gè)引用。
static_cast
static_cast < new_type > ( expression )
本來(lái)應(yīng)該先討論dynamic_cast的,因?yàn)樵蹅儽緛?lái)聊的RTTI嘛,但是先了解一下static_cast,然后看和dynamic_cast的比較可能更好一點(diǎn)。
使用場(chǎng)景:
- 基本數(shù)據(jù)類型之間的轉(zhuǎn)換
- initializing conversion
int n = static_cast<int>(3.14); cout << "n = " << n << '\n'; vector<int> v = static_cast<vector<int>>(10); cout << "v.size() = " << v.size() << '\n';
- 類指針或引用向下轉(zhuǎn)換。
- 將類型轉(zhuǎn)為右值類型,進(jìn)行move操作,這個(gè)在標(biāo)準(zhǔn)庫(kù)中有體現(xiàn)(放在下一篇文章中解釋)
vector<int> v = static_cast<vector<int>>(10); vector<int> v2 = static_cast<vector<int>&&>(v); cout << "after move, v.size() = " << v.size() << '\n'; cout << v.size() << endl;
子類數(shù)組指針向上轉(zhuǎn)成父類的指針
struct B { int m = 0; void hello() const { cout << "Hello world, this is B!\n"; } }; struct D : B { void hello() const { cout << "Hello world, this is D!\n"; } }; D a[10]; B* dp = static_cast<B*>(a); dp->hello();
枚舉轉(zhuǎn)換成int or float
enum E { ONE = 1, TWO, THREE }; E e = E::ONE; int one = static_cast<int>(e); cout << one << '\n';
int to enum, enum to another emum
enum class E { ONE = 1, TWO, THREE }; enum EU { ONE = 1, TWO, THREE }; E e = E::ONE; int one = static_cast<int>(e); E e2 = static_cast<E>(one); EU eu = static_cast<EU>(e2);
void* to any type
int a = 100; void* voidp = &a; int *p = static_cast<int*>(voidp);
注意:
- static_cast不能轉(zhuǎn)換掉expression的const、volatile和__unaligned屬性,
- 編譯器隱式執(zhí)行任何類型轉(zhuǎn)換都可由static_cast顯示完成
dynamic_cast
dynamic_cast < new-type > ( expression )
接下來(lái)是dynamic_cast:
動(dòng)態(tài)映射可以映射到中間層級(jí),將派生類映射到任何一個(gè)基類,然后在基類之間可以相互映射。
dynamic_cast實(shí)現(xiàn)原理: 先恢復(fù)源指針的RTTI信息,再取目標(biāo)的RTTT信息,比較兩者是否相同,不同取目標(biāo)類型的基類; 由于它需要檢查一長(zhǎng)串基類列表,故動(dòng)態(tài)映射的開(kāi)銷比typeid大。
dynamic_cast的安全性: 如實(shí)現(xiàn)原理所說(shuō),dynamic_cast會(huì)做一系列的類型檢查,轉(zhuǎn)換成功會(huì)返回目標(biāo)類型指針,失敗則會(huì)返回NULL, 相對(duì)于static_cast安全,因?yàn)?static_cast即使轉(zhuǎn)換失敗也不會(huì)返回NULL。
例2: 這種情況下 static_cast() 也是安全的。
class Base { public: void func() { cout << "Base func" << endl; } }; class Derived : public Base { public: void func() { cout << "Derived func" << endl; } }; int main(int argc, char const* argv[]) { Derived *pd = new Derived; pd->func(); Base* pb1 = dynamic_cast<Base*>(pd); pb1->func(); Base* pb2 = static_cast<Base*>(pd); pb2->func(); return 0; }
pd 指針指向的內(nèi)存是子類對(duì)象,我們知道,繼承子類是包含父類的,相當(dāng)于在父類的基礎(chǔ)上在添加子類的成員(如果你還不清楚的話,建議你看一下我之前的文章: 虛函數(shù),虛表深度剖析)。 所以pd指針轉(zhuǎn)成父類指針也是沒(méi)問(wèn)題的,static_cast也一樣安全。
相反,如果指針指向的內(nèi)存是父類成員,轉(zhuǎn)成子類指針,dynamic_cast 則會(huì)失敗,返回NULL, 但是static_cast不會(huì)失敗,強(qiáng)制轉(zhuǎn)過(guò)去了,如果此時(shí)子類指針訪問(wèn)父類中不存在,但是子類中存在的成員,則會(huì)發(fā)生意想不到的問(wèn)題。
看下面這個(gè)例子:
class Base { public: virtual void func() { cout << "Base func" << endl; } }; class Derived : public Base { public: virtual void func() { cout << "Derived func" << endl; } int m_value = 0; }; int main(int argc, char const* argv[]) { Base *pb = new Base; pb->func(); Derived* pd1 = dynamic_cast<Derived*>(pb); if (pd1 != NULL) { pd1->func(); } else { cout << "dynamic_cast failed" << endl; } Derived* pd2 = static_cast<Derived*>(pb); pd2->func(); cout << "m_value: " << pd2->m_value << endl; return 0; }
輸出:
Base func
dynamic_cast failed
Base func // 父類中也有這個(gè)虛函數(shù),所以static_cast轉(zhuǎn)換調(diào)用沒(méi)出問(wèn)題。
m_value: -33686019 // 這里出問(wèn)題了
對(duì)于上行轉(zhuǎn)換,static_cast 和 dynamic_cast 效果一樣,都安全, 如果只是單純的向上轉(zhuǎn)的話,沒(méi)必要,直接用虛函數(shù)實(shí)現(xiàn)就好了。
對(duì)于下行轉(zhuǎn)換: 你必須確定要轉(zhuǎn)換的數(shù)據(jù)確實(shí)是目標(biāo)類型的數(shù)據(jù),即需要注意要轉(zhuǎn)換的父類類型指針是否真的指向子類對(duì)象,如果是,static_cast和dynamic_cast都能成功;如果不是static_cast能返回,但是不安全,可能會(huì)出現(xiàn)訪問(wèn)越界錯(cuò)誤,而dynamic_cast在運(yùn)行時(shí)類型檢查過(guò)程中,判定該過(guò)程不能轉(zhuǎn)換,返回NULL。
const_cast
const_cast < new_type > ( expression )
上面講了static_cast是不能去掉 const,而 const_cast是專門用來(lái)去掉 const。
而添加const, static_cast 也是可以添加上 const,只是不能去掉const
看下面一個(gè)例子:
const int a = 26; const int* pa = &a; //*pa = 3; // 編譯不過(guò),指針常量不能通過(guò)指針修改值 int* b = const_cast<int*>(pa); // 把const轉(zhuǎn)換掉 *b = 3; cout << "a: " << a << endl; // 26 cout << "*b: " << *b << endl; // 3
a 為 const int 類型,不可修改,pa 為 const int* 類型,不能通過(guò)pa指針修改a的值
b 通過(guò) const_cast 轉(zhuǎn)換掉了const,成功修改了 a 的值。
有一個(gè)問(wèn)題,為什么a輸出是26呢?
如果存在const int x = 26; 這種情況,那么編譯器會(huì)認(rèn)為x是一個(gè)編譯期可計(jì)算出結(jié)果的常量,那么x就會(huì)像宏定義一樣,用到x的地方會(huì)被編譯器替換成26。
上述這個(gè)例子不建議使用,因?yàn)?a 聲明為 const int 類型,實(shí)際上是并不希望被修改的,這樣強(qiáng)行修改可能會(huì)導(dǎo)致項(xiàng)目里不可預(yù)期的錯(cuò)誤。
const_cast 的使用場(chǎng)景
如果有一個(gè)函數(shù),它的形參是non-const類型變量,而且函數(shù)不會(huì)對(duì)實(shí)參的值進(jìn)行改動(dòng),這時(shí)我們可以使用類型為const的變量來(lái)調(diào)用函數(shù)。
void func(int* a) { } int main() { const int a = 26; const int* pa = &a; func(const_cast<int*>(pa)); }
這種情況其實(shí)我覺(jué)得沒(méi)必要,實(shí)際上我不想改的話,我形參加 const,把前提推翻不就行了,還安全。
定義了一個(gè)non-const的變量,卻使用了一個(gè)指向const值的指針來(lái)指向它(這不是沒(méi)事找事嘛),在程序的某處我們想改變這個(gè)變量的值了,但手頭只持有指針,這時(shí)const_cast就可以用到了
int a = 26; const int* pa = &a; // *pa = 1; 編譯不過(guò) int* pa2 = const_cast<int*>(pa); *pa2 = 1;
reinterpret_cast
reinterpret_cast < new_type > ( expression )
reinterpret_ cast 通常為操作數(shù)的位模式提供較低層的重新解釋。
看這個(gè)例子:
int n = 9; double d = static_cast<double>(n); cout << n << " " << d; // 輸出9 9 int n2 = 9; double d2 = reinterpret_cast<double&>(n2); cout << n2 << " " << d2; // 輸出9 -9.25596e+61
上面的例子中,我們將一個(gè)變址從int 轉(zhuǎn)換到 double。這些類型的二進(jìn)制表達(dá)式是不同的。要將整數(shù)9轉(zhuǎn)換到雙精度整數(shù)9, static_cast需要正確地為雙精度整數(shù)d補(bǔ)足比特位。其結(jié)果為9.0。
而 reinterpret_cast 的行為卻不同,僅僅是把內(nèi)存拷貝到目標(biāo)空間,解釋出來(lái)是一個(gè)大數(shù)。
reinterpret_cast這個(gè)操作符被用于的類型轉(zhuǎn)換的轉(zhuǎn)換結(jié)果幾乎都是未知的。
使用 reinterpret_cast 的代碼很難移植。轉(zhuǎn)換函數(shù)指針的代碼是不可移植的,(C++不保證所有的函數(shù)指針都被用一樣的方法表示),在一些情況下這樣的轉(zhuǎn)換會(huì)產(chǎn)生不正確的結(jié)果。 所以應(yīng)該避免轉(zhuǎn)換函數(shù)指針類型,按照C++新思維的話來(lái)說(shuō),reinterpret_cast 是為了映射到一個(gè)完全不同類型的意思,這個(gè)關(guān)鍵詞在我們需要把類型映射回原有類型時(shí)用到它。
我們映射到的類型僅僅是為了故弄玄虛和其他目的,這是所有映射中最危險(xiǎn)的。reinterpret_cast就是一把銳利無(wú)比的雙刃劍,除非你處于背水一戰(zhàn)和火燒眉毛的危急時(shí)刻,否則絕不能使用。
其實(shí) reinterpret_cast 用法細(xì)節(jié)還有不少,什么時(shí)候需要用到,再去官方了解一下就好了,現(xiàn)在糾的太細(xì)意義不大。
總結(jié)
到此這篇關(guān)于C++ RTTI與4種類型轉(zhuǎn)換的文章就介紹到這了,更多相關(guān)C++ RTTI與類型轉(zhuǎn)換內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于C++數(shù)組中重復(fù)的數(shù)字
這篇文章主要介紹得是關(guān)于C++數(shù)組中重復(fù)的數(shù)字,文章以問(wèn)題描述得形式,對(duì)問(wèn)題展開(kāi)分析用不同得方法去解決問(wèn)題并附上方法得詳細(xì)代碼,需要的朋友可以參考以下文章得具體內(nèi)容2021-11-11C語(yǔ)言實(shí)現(xiàn)abs和fabs絕對(duì)值
這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)abs和fabs絕對(duì)值,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01wxWidgets實(shí)現(xiàn)無(wú)標(biāo)題欄窗口拖動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了wxWidgets實(shí)現(xiàn)無(wú)標(biāo)題欄窗口拖動(dòng)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02C語(yǔ)言實(shí)現(xiàn)掃雷小游戲(擴(kuò)展版)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)擴(kuò)展版的掃雷小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05C++實(shí)現(xiàn)含附件的郵件發(fā)送功能
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)含附件的郵件發(fā)送功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05