詳解C++中的四種類型轉(zhuǎn)換運(yùn)算符
隱式類型轉(zhuǎn)換是安全的,顯式類型轉(zhuǎn)換是有風(fēng)險(xiǎn)的,C語(yǔ)言之所以增加強(qiáng)制類型轉(zhuǎn)換的語(yǔ)法,就是為了強(qiáng)調(diào)風(fēng)險(xiǎn),讓程序員意識(shí)到自己在做什么。但是,這種強(qiáng)調(diào)風(fēng)險(xiǎn)的方式還是比較粗放,粒度比較大,它并沒有表明存在什么風(fēng)險(xiǎn),風(fēng)險(xiǎn)程度如何。
再者,C風(fēng)格的強(qiáng)制類型轉(zhuǎn)換統(tǒng)一使用( )
,而( )
在代碼中隨處可見,所以也不利于使用文本檢索工具(例如 Windows 下的 Ctrl+F、Linux 下的 grep 命令、Mac 下的 Command+F)定位關(guān)鍵代碼。為了使?jié)撛陲L(fēng)險(xiǎn)更加細(xì)化,使問題追溯更加方便,使書寫格式更加規(guī)范,C++ 對(duì)類型轉(zhuǎn)換進(jìn)行了分類,并新增了四個(gè)關(guān)鍵字來(lái)予以支持,它們分別是:
這四個(gè)關(guān)鍵字的語(yǔ)法格式都是一樣的,具體為:
xxx_cast(data)
newType 是要轉(zhuǎn)換成的新類型,data 是被轉(zhuǎn)換的數(shù)據(jù)。例如,老式的C風(fēng)格的 double 轉(zhuǎn) int 的寫法為:
double scores = 95.5; int n = (int)scores;
C++ 新風(fēng)格的寫法為:
double scores = 95.5; int n = static_cast<int>(scores);
static_cast 關(guān)鍵字
static_cast 只能用于良性轉(zhuǎn)換,這樣的轉(zhuǎn)換風(fēng)險(xiǎn)較低,一般不會(huì)發(fā)生什么意外,例如:
- 原有的自動(dòng)類型轉(zhuǎn)換,例如 short 轉(zhuǎn) int、int 轉(zhuǎn) double、const 轉(zhuǎn)非 const、向上轉(zhuǎn)型等;
- void 指針和具體類型指針之間的轉(zhuǎn)換,例如
void *
轉(zhuǎn)int *
、char *
轉(zhuǎn)void *
等; - 有轉(zhuǎn)換構(gòu)造函數(shù)或者類型轉(zhuǎn)換函數(shù)的類與其它類型之間的轉(zhuǎn)換,例如 double 轉(zhuǎn) Complex(調(diào)用轉(zhuǎn)換構(gòu)造函數(shù))、Complex 轉(zhuǎn) double(調(diào)用類型轉(zhuǎn)換函數(shù))。
需要注意的是,static_cast 不能用于無(wú)關(guān)類型之間的轉(zhuǎn)換,因?yàn)檫@些轉(zhuǎn)換都是有風(fēng)險(xiǎn)的,例如:
- 兩個(gè)具體類型指針之間的轉(zhuǎn)換,例如
int *
轉(zhuǎn)double *
、Student *
轉(zhuǎn)int *
等。不同類型的數(shù)據(jù)存儲(chǔ)格式不一樣,長(zhǎng)度也不一樣,用 A 類型的指針指向 B 類型的數(shù)據(jù)后,會(huì)按照 A 類型的方式來(lái)處理數(shù)據(jù):如果是讀取操作,可能會(huì)得到一堆沒有意義的值;如果是寫入操作,可能會(huì)使 B 類型的數(shù)據(jù)遭到破壞,當(dāng)再次以 B 類型的方式讀取數(shù)據(jù)時(shí)會(huì)得到一堆沒有意義的值。 - int 和指針之間的轉(zhuǎn)換。將一個(gè)具體的地址賦值給指針變量是非常危險(xiǎn)的,因?yàn)樵摰刂飞系膬?nèi)存可能沒有分配,也可能沒有讀寫權(quán)限,恰好是可用內(nèi)存反而是小概率事件。
static_cast 也不能用來(lái)去掉表達(dá)式的 const 修飾和 volatile 修飾。換句話說,不能將 const/volatile 類型轉(zhuǎn)換為非 const/volatile 類型。static_cast 是“靜態(tài)轉(zhuǎn)換”的意思,也就是在編譯期間轉(zhuǎn)換,轉(zhuǎn)換失敗的話會(huì)拋出一個(gè)編譯錯(cuò)誤。下面的代碼演示了 static_cast 的正確用法和錯(cuò)誤用法:
#include <iostream> #include <cstdlib> using namespace std; class Complex{ public: Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ } public: operator double() const { return m_real; } //類型轉(zhuǎn)換函數(shù) private: double m_real; double m_imag; }; int main(){ //下面是正確的用法 int m = 100; Complex c(12.5, 23.8); long n = static_cast<long>(m); //寬轉(zhuǎn)換,沒有信息丟失 char ch = static_cast<char>(m); //窄轉(zhuǎn)換,可能會(huì)丟失信息 int *p1 = static_cast<int*>( malloc(10 * sizeof(int)) ); //將void指針轉(zhuǎn)換為具體類型指針 void *p2 = static_cast<void*>(p1); //將具體類型指針,轉(zhuǎn)換為void指針 double real= static_cast<double>(c); //調(diào)用類型轉(zhuǎn)換函數(shù) //下面的用法是錯(cuò)誤的 float *p3 = static_cast<float*>(p1); //不能在兩個(gè)具體類型的指針之間進(jìn)行轉(zhuǎn)換 p3 = static_cast<float*>(0X2DF9); //不能將整數(shù)轉(zhuǎn)換為指針類型 return 0; }
const_cast 關(guān)鍵字
const_cast 比較好理解,它用來(lái)去掉表達(dá)式的 const 修飾或 volatile 修飾。換句話說,const_cast 就是用來(lái)將 const/volatile 類型轉(zhuǎn)換為非 const/volatile 類型。
下面我們以 const 為例來(lái)說明 const_cast 的用法:
#include <iostream> using namespace std; int main(){ const int n = 100; int *p = const_cast<int*>(&n); *p = 234; cout<<"n = "<<n<<endl; cout<<"*p = "<<*p<<endl; return 0; }
運(yùn)行結(jié)果:
n = 100
*p = 234
&n
用來(lái)獲取 n 的地址,它的類型為const int *
,必須使用 const_cast 轉(zhuǎn)換為int *
類型后才能賦值給 p。由于 p 指向了 n,并且 n 占用的是棧內(nèi)存,有寫入權(quán)限,所以可以通過 p 修改 n 的值。有讀者可能會(huì)問,為什么通過 n 和 *p 輸出的值不一樣呢?這是因?yàn)?C++ 對(duì)常量的處理更像是編譯時(shí)期的#define
,是一個(gè)值替換的過程,代碼中所有使用 n 的地方在編譯期間就被替換成了 100。換句話說,第 8 行代碼被修改成了下面的形式:
cout<<"n = "<<100<<endl;
這樣以來(lái),即使程序在運(yùn)行期間修改 n 的值,也不會(huì)影響 cout 語(yǔ)句了。更多關(guān)于 const 的內(nèi)容請(qǐng)猛擊《C++中的const又玩出了新花樣》。使用 const_cast 進(jìn)行強(qiáng)制類型轉(zhuǎn)換可以突破 C/C++ 的常數(shù)限制,修改常數(shù)的值,因此有一定的危險(xiǎn)性;但是程序員如果這樣做的話,基本上會(huì)意識(shí)到這個(gè)問題,因此也還有一定的安全性。
reinterpret_cast 關(guān)鍵字
reinterpret 是“重新解釋”的意思,顧名思義,reinterpret_cast 這種轉(zhuǎn)換僅僅是對(duì)二進(jìn)制位的重新解釋,不會(huì)借助已有的轉(zhuǎn)換規(guī)則對(duì)數(shù)據(jù)進(jìn)行調(diào)整,非常簡(jiǎn)單粗暴,所以風(fēng)險(xiǎn)很高。
reinterpret_cast 可以認(rèn)為是 static_cast 的一種補(bǔ)充,一些 static_cast 不能完成的轉(zhuǎn)換,就可以用 reinterpret_cast 來(lái)完成,例如兩個(gè)具體類型指針之間的轉(zhuǎn)換、int 和指針之間的轉(zhuǎn)換(有些編譯器只允許 int 轉(zhuǎn)指針,不允許反過來(lái))。
下面的代碼代碼演示了 reinterpret_cast 的使用:
#include <iostream> using namespace std; class A{ public: A(int a = 0, int b = 0): m_a(a), m_b(b){} private: int m_a; int m_b; }; int main(){ //將 char* 轉(zhuǎn)換為 float* char str[]="http://c.biancheng.net"; float *p1 = reinterpret_cast<float*>(str); cout<<*p1<<endl; //將 int 轉(zhuǎn)換為 int* int *p = reinterpret_cast<int*>(100); //將 A* 轉(zhuǎn)換為 int* p = reinterpret_cast<int*>(new A(25, 96)); cout<<*p<<endl; return 0; }
運(yùn)行結(jié)果:3.0262e+2925可以想象,用一個(gè) float 指針來(lái)操作一個(gè) char 數(shù)組是一件多么荒誕和危險(xiǎn)的事情,這樣的轉(zhuǎn)換方式不到萬(wàn)不得已的時(shí)候不要使用。將A*
轉(zhuǎn)換為int*
,使用指針直接訪問 private 成員刺穿了一個(gè)類的封裝性,更好的辦法是讓類提供 get/set 函數(shù),間接地訪問成員變量。
dynamic_cast 關(guān)鍵字
dynamic_cast 用于在類的繼承層次之間進(jìn)行類型轉(zhuǎn)換,它既允許向上轉(zhuǎn)型(Upcasting),也允許向下轉(zhuǎn)型(Downcasting)。向上轉(zhuǎn)型是無(wú)條件的,不會(huì)進(jìn)行任何檢測(cè),所以都能成功;向下轉(zhuǎn)型的前提必須是安全的,要借助 RTTI 進(jìn)行檢測(cè),所有只有一部分能成功。
dynamic_cast 與 static_cast 是相對(duì)的,dynamic_cast 是“動(dòng)態(tài)轉(zhuǎn)換”的意思,static_cast 是“靜態(tài)轉(zhuǎn)換”的意思。dynamic_cast 會(huì)在程序運(yùn)行期間借助 RTTI 進(jìn)行類型轉(zhuǎn)換,這就要求基類必須包含虛函數(shù);static_cast 在編譯期間完成類型轉(zhuǎn)換,能夠更加及時(shí)地發(fā)現(xiàn)錯(cuò)誤。
dynamic_cast 的語(yǔ)法格式為:
dynamic_cast (expression)
newType 和 expression 必須同時(shí)是指針類型或者引用類型。換句話說,dynamic_cast 只能轉(zhuǎn)換指針類型和引用類型,其它類型(int、double、數(shù)組、類、結(jié)構(gòu)體等)都不行。對(duì)于指針,如果轉(zhuǎn)換失敗將返回 NULL;對(duì)于引用,如果轉(zhuǎn)換失敗將拋出std::bad_cast
異常。
1) 向上轉(zhuǎn)型(Upcasting)
向上轉(zhuǎn)型時(shí),只要待轉(zhuǎn)換的兩個(gè)類型之間存在繼承關(guān)系,并且基類包含了虛函數(shù)(這些信息在編譯期間就能確定),就一定能轉(zhuǎn)換成功。因?yàn)橄蛏限D(zhuǎn)型始終是安全的,所以 dynamic_cast 不會(huì)進(jìn)行任何運(yùn)行期間的檢查,這個(gè)時(shí)候的 dynamic_cast 和 static_cast 就沒有什么區(qū)別了。
「向上轉(zhuǎn)型時(shí)不執(zhí)行運(yùn)行期檢測(cè)」雖然提高了效率,但也留下了安全隱患,請(qǐng)看下面的代碼:
#include <iostream> #include <iomanip> using namespace std; class Base{ public: Base(int a = 0): m_a(a){ } int get_a() const{ return m_a; } virtual void func() const { } protected: int m_a; }; class Derived: public Base{ public: Derived(int a = 0, int b = 0): Base(a), m_b(b){ } int get_b() const { return m_b; } private: int m_b; }; int main(){ //情況① Derived *pd1 = new Derived(35, 78); Base *pb1 = dynamic_cast<Derived*>(pd1); cout<<"pd1 = "<<pd1<<", pb1 = "<<pb1<<endl; cout<<pb1->get_a()<<endl; pb1->func(); //情況② int n = 100; Derived *pd2 = reinterpret_cast<Derived*>(&n); Base *pb2 = dynamic_cast<Base*>(pd2); cout<<"pd2 = "<<pd2<<", pb2 = "<<pb2<<endl; cout<<pb2->get_a()<<endl; //輸出一個(gè)垃圾值 pb2->func(); //內(nèi)存錯(cuò)誤 return 0; }
情況①是正確的,沒有任何問題。對(duì)于情況②,pd 指向的是整型變量 n,并沒有指向一個(gè) Derived 類的對(duì)象,在使用 dynamic_cast 進(jìn)行類型轉(zhuǎn)換時(shí)也沒有檢查這一點(diǎn),而是將 pd 的值直接賦給了 pb(這里并不需要調(diào)整偏移量),最終導(dǎo)致 pb 也指向了 n。因?yàn)?pb 指向的不是一個(gè)對(duì)象,所以get_a()
得不到 m_a 的值(實(shí)際上得到的是一個(gè)垃圾值),pb2->func()
也得不到 func() 函數(shù)的正確地址。
pb2->func()得不到 func() 的正確地址的原因在于,pb2 指向的是一個(gè)假的“對(duì)象”,它沒有虛函數(shù)表,也沒有虛函數(shù)表指針,而 func() 是虛函數(shù),必須到虛函數(shù)表中才能找到它的地址。
2) 向下轉(zhuǎn)型(Downcasting)
向下轉(zhuǎn)型是有風(fēng)險(xiǎn)的,dynamic_cast 會(huì)借助 RTTI 信息進(jìn)行檢測(cè),確定安全的才能轉(zhuǎn)換成功,否則就轉(zhuǎn)換失敗。那么,哪些向下轉(zhuǎn)型是安全地呢,哪些又是不安全的呢?下面我們通過一個(gè)例子來(lái)演示:
#include <iostream> using namespace std; class A{ public: virtual void func() const { cout<<"Class A"<<endl; } private: int m_a; }; class B: public A{ public: virtual void func() const { cout<<"Class B"<<endl; } private: int m_b; }; class C: public B{ public: virtual void func() const { cout<<"Class C"<<endl; } private: int m_c; }; class D: public C{ public: virtual void func() const { cout<<"Class D"<<endl; } private: int m_d; }; int main(){ A *pa = new A(); B *pb; C *pc; //情況① pb = dynamic_cast<B*>(pa); //向下轉(zhuǎn)型失敗 if(pb == NULL){ cout<<"Downcasting failed: A* to B*"<<endl; }else{ cout<<"Downcasting successfully: A* to B*"<<endl; pb -> func(); } pc = dynamic_cast<C*>(pa); //向下轉(zhuǎn)型失敗 if(pc == NULL){ cout<<"Downcasting failed: A* to C*"<<endl; }else{ cout<<"Downcasting successfully: A* to C*"<<endl; pc -> func(); } cout<<"-------------------------"<<endl; //情況② pa = new D(); //向上轉(zhuǎn)型都是允許的 pb = dynamic_cast<B*>(pa); //向下轉(zhuǎn)型成功 if(pb == NULL){ cout<<"Downcasting failed: A* to B*"<<endl; }else{ cout<<"Downcasting successfully: A* to B*"<<endl; pb -> func(); } pc = dynamic_cast<C*>(pa); //向下轉(zhuǎn)型成功 if(pc == NULL){ cout<<"Downcasting failed: A* to C*"<<endl; }else{ cout<<"Downcasting successfully: A* to C*"<<endl; pc -> func(); } return 0; }
運(yùn)行結(jié)果:
Downcasting failed: A* to B*
Downcasting failed: A* to C*
Downcasting successfully: A* to B*
ClassD
Downcasting successfully: A* to C*
Class D
這段代碼中類的繼承順序?yàn)椋篈 --> B --> C --> D。pa 是A*
類型的指針,當(dāng) pa 指向 A 類型的對(duì)象時(shí),向下轉(zhuǎn)型失敗,pa 不能轉(zhuǎn)換為B*
或C*
類型。當(dāng) pa 指向 D 類型的對(duì)象時(shí),向下轉(zhuǎn)型成功,pa 可以轉(zhuǎn)換為B*
或C*
類型。同樣都是向下轉(zhuǎn)型,為什么 pa 指向的對(duì)象不同,轉(zhuǎn)換的結(jié)果就大相徑庭呢?
在《C++ RTTI機(jī)制下的對(duì)象內(nèi)存模型(透徹)》一節(jié)中,我們講到了有虛函數(shù)存在時(shí)對(duì)象的真實(shí)內(nèi)存模型,并且也了解到,每個(gè)類都會(huì)在內(nèi)存中保存一份類型信息,編譯器會(huì)將存在繼承關(guān)系的類的類型信息使用指針“連接”起來(lái),從而形成一個(gè)繼承鏈(Inheritance Chain),也就是如下圖所示的樣子:
當(dāng)使用 dynamic_cast 對(duì)指針進(jìn)行類型轉(zhuǎn)換時(shí),程序會(huì)先找到該指針指向的對(duì)象,再根據(jù)對(duì)象找到當(dāng)前類(指針指向的對(duì)象所屬的類)的類型信息,并從此節(jié)點(diǎn)開始沿著繼承鏈向上遍歷,如果找到了要轉(zhuǎn)化的目標(biāo)類型,那么說明這種轉(zhuǎn)換是安全的,就能夠轉(zhuǎn)換成功,如果沒有找到要轉(zhuǎn)換的目標(biāo)類型,那么說明這種轉(zhuǎn)換存在較大的風(fēng)險(xiǎn),就不能轉(zhuǎn)換。
對(duì)于本例中的情況①,pa 指向 A 類對(duì)象,根據(jù)該對(duì)象找到的就是 A 的類型信息,當(dāng)程序從這個(gè)節(jié)點(diǎn)開始向上遍歷時(shí),發(fā)現(xiàn) A 的上方?jīng)]有要轉(zhuǎn)換的 B 類型或 C 類型(實(shí)際上 A 的上方?jīng)]有任何類型了),所以就轉(zhuǎn)換敗了。對(duì)于情況②,pa 指向 D 類對(duì)象,根據(jù)該對(duì)象找到的就是 D 的類型信息,程序從這個(gè)節(jié)點(diǎn)向上遍歷的過程中,發(fā)現(xiàn)了 C 類型和 B 類型,所以就轉(zhuǎn)換成功了。
總起來(lái)說,dynamic_cast 會(huì)在程序運(yùn)行過程中遍歷繼承鏈,如果途中遇到了要轉(zhuǎn)換的目標(biāo)類型,那么就能夠轉(zhuǎn)換成功,如果直到繼承鏈的頂點(diǎn)(最頂層的基類)還沒有遇到要轉(zhuǎn)換的目標(biāo)類型,那么就轉(zhuǎn)換失敗。對(duì)于同一個(gè)指針(例如 pa),它指向的對(duì)象不同,會(huì)導(dǎo)致遍歷繼承鏈的起點(diǎn)不一樣,途中能夠匹配到的類型也不一樣,所以相同的類型轉(zhuǎn)換產(chǎn)生了不同的結(jié)果。從表面上看起來(lái) dynamic_cast 確實(shí)能夠向下轉(zhuǎn)型,本例也很好地證明了這一點(diǎn):B 和 C 都是 A 的派生類,我們成功地將 pa 從 A 類型指針轉(zhuǎn)換成了 B 和 C 類型指針。
但是從本質(zhì)上講,dynamic_cast 還是只允許向上轉(zhuǎn)型,因?yàn)樗粫?huì)向上遍歷繼承鏈。造成這種假象的根本原因在于,派生類對(duì)象可以用任何一個(gè)基類的指針指向它,這樣做始終是安全的。本例中的情況②,pa 指向的對(duì)象是 D 類型的,pa、pb、pc 都是 D 的基類的指針,所以它們都可以指向 D 類型的對(duì)象,dynamic_cast 只是讓不同的基類指針指向同一個(gè)派生類對(duì)象罷了。
以上就是詳解C++中的四種類型轉(zhuǎn)換運(yùn)算符的詳細(xì)內(nèi)容,更多關(guān)于C++轉(zhuǎn)換運(yùn)算符的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解C語(yǔ)言中動(dòng)態(tài)內(nèi)存管理
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言中動(dòng)態(tài)內(nèi)存管理的相關(guān)知識(shí),以及常見的動(dòng)態(tài)內(nèi)存的錯(cuò)誤,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-07-07C++ Boost實(shí)現(xiàn)異步端口掃描器詳解
端口掃描是一種用于識(shí)別目標(biāo)系統(tǒng)上哪些網(wǎng)絡(luò)端口處于開放、關(guān)閉或監(jiān)聽狀態(tài)的網(wǎng)絡(luò)活動(dòng),本文將運(yùn)用Boost框架實(shí)現(xiàn)一個(gè)基于TCP的掃描工具,有需要的小伙伴可以參考下2023-11-11c++實(shí)現(xiàn)strcat字符串連接庫(kù)函數(shù)的方法詳解
本篇文章是對(duì)使用c++實(shí)現(xiàn)strcat字符串連接庫(kù)函數(shù)的方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05C++?opencv學(xué)習(xí)之圖像像素的邏輯操作
圖像的像素操作包括讀寫操作、算數(shù)操作、邏輯運(yùn)算操作等,下面這篇文章主要給大家介紹了關(guān)于C++?opencv學(xué)習(xí)之圖像像素的邏輯操作的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11OpenCV中C++函數(shù)imread讀取圖片的問題及解決方法
利用C++函數(shù)imread讀取圖片的時(shí)候返回的結(jié)果總是空,而利用C函數(shù)cvLoadImage時(shí)卻能讀取到圖像。怎么回事?今天小編通過本教程給大家簡(jiǎn)單說明原因2017-03-03C++設(shè)計(jì)模式編程中使用Bridge橋接模式的完全攻略
這篇文章主要介紹了C++設(shè)計(jì)模式編程中使用Bridge橋接模式的完全攻略,Bridge將抽象部分與它的實(shí)現(xiàn)部分分離,使它們都可以獨(dú)立地變化需要的朋友可以參考下2016-03-03