關鍵字③:類型轉換

  • 隱式轉換

1)算數轉換:在混合類型的算術表達式中, 最寬的數據類型成爲目標轉換類型。

int int_type = 3;
double double_type = 4.0012;

int_type + double_type = ?

上面定義了整型變量int_type,也定義了雙精度型變量double_type,兩者進行相加;根據類型轉換,隱式轉換過程中,是向着op操作中類型高進行轉換,故最終得到的結果的類型是雙精度型(double)變量。

 

2)一種類型表達式賦值給另一種類型的對象:目標類型是被賦值對象的類型

int *pi = 3;

int_type = double_type 
int_type ?

上面定義了整型指針變量,則此時的3的類型是int*類型;而將雙精度類型賦值給整型類型,此時類型轉換將由雙精度(double)轉化成整型(int)

 

3)將一個表達式作爲實參傳遞給函數調用,此時形參和實參類型不一致:目標轉換類型爲形參的類型

extern double sqrt(double);

cout << "The square root of 2 is " << sqrt(2) << endl;

定義一個sqrt函數,傳入的形參爲雙精度類型,返回值亦是雙精度類型,在調用過程中,我們傳入的是整型變量2;這時候的整型變量會自動由整型(int)提升爲雙精度型(double)

 

4)從一個函數返回一個表達式,表達式類型與返回類型不一致:目標轉換類型爲函數的返回類型

double difference(int ival1, int ival2)
{
    return ival1 - ival2;
    //返回值被提升爲double類型
}

 

  • 顯式轉換

C++中共有四種顯示轉換的關鍵字,分別是:

  • static_cast;
  • dynamic_cast;
  • reinterpret_cast;
  • const_cast;

這四個關鍵字的語法格式都是一樣的,具體爲:

xxx_cast<newType>(data)

參數解析:

  1. newtype:需要轉化的新的類型;
  2. data:需要轉化的原類型數據;

老式的C風格的 double 轉 int 的寫法爲:

double scores = 95.5;
int n = (int)scores;

C++的新風格轉化爲:

double scores = 95.5;
int n = static_cast<int>(scores);

 

  • static_cast

static_cast 只能用於良性轉換,這樣的轉換風險較低,一般不會發生什麼意外,例如:

  • 原有的自動類型轉換,例如 short 轉 int、int 轉 double、const 轉非 const、向上轉型等;
  • void 指針和具體類型指針之間的轉換,例如 void * 轉 int *,char * 轉 void * 等;
  • 有轉換構造函數或者類型轉換函數的類與其它類型之間的轉換,例如 double 轉 Complex(調用轉換構造函數)、Complex 轉 double(調用類型轉換函數)。

需要注意的是,static_cast 不能用於無關類型之間的轉換,因爲這些轉換都是有風險的,例如:

  • 兩個具體類型指針之間的轉換,例如 int * 轉 double*、Student * 轉 int * 等。不同類型的數據存儲格式不一樣,長度也不一樣,用A類型的指針指向B類型的數據後,會按照A類型的方式來處理數據:如果是讀取操作,可能會得到一堆沒有意義的值;如果是寫入操作,可能會使B類型的數據遭到破壞,當再次以B類型的方式讀取數據時會得到一堆沒有意義的值;
  • int 和指針之間的轉換。將一個具體的地址賦值給指針變量是非常危險的,因爲該地址上的內存可能沒有分配,也可能沒有讀寫權限,恰好是可用內存反而是小概率事件;

【注】

  1. static_cast 也不能用來去掉表達式的 const 修飾和 volatile 修飾。換句話說,不能將 const/volatile 類型轉換爲非 const/volatile 類型;
  2. static_cast 是“靜態轉換”的意思,也就是在編譯期間轉換,轉換失敗的話會拋出一個編譯錯誤;

 

它主要有如下幾種用法:

  • 用於類層次結構中基類和子類之間指針或引用的轉換。進行上行轉換(把子類的指針或引用轉換成基類表示)是安全的;進行下行轉換(把基類指針或引用轉換成子類指針或引用)時,由於沒有動態類型檢查,所以是不安全的;
  • 用於基本數據類型之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性也要開發人員來保證;
  • 把void指針轉換成目標類型的指針(不安全!!)
  • 把任何類型的表達式轉換成void類型;

具體用法如下:

#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; }  //類型轉換函數
private:
    double m_real;
    double m_imag;
};

int main(){
    //下面是正確的用法
    int m = 100;
    Complex c(12.5, 23.8);
    long n = static_cast<long>(m);  //寬轉換,沒有信息丟失
    char ch = static_cast<char>(m);  //窄轉換,可能會丟失信息
    int *p1 = static_cast<int*>( malloc(10 * sizeof(int)) );  //將void指針轉換爲具體類型指針
    void *p2 = static_cast<void*>(p1);  //將具體類型指針,轉換爲void指針
    double real= static_cast<double>(c);  //調用類型轉換函數
   
    //下面的用法是錯誤的
    float *p3 = static_cast<float*>(p1);  //不能在兩個具體類型的指針之間進行轉換
    p3 = static_cast<float*>(0X2DF9);  //不能將整數轉換爲指針類型

    return 0;
}

 

  • const_cast 

const_cast 比較好理解,它用來去掉表達式的 const 修飾或 volatile 修飾。換句話說,const_cast 就是用來將 const/volatile 類型轉換爲非 const/volatile 類型。

具體用法如下:

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

 

  • reinterpret_cast 

reinterpret 是“重新解釋”的意思,顧名思義,reinterpret_cast 這種轉換僅僅是對二進制位的重新解釋,不會藉助已有的轉換規則對數據進行調整,非常簡單粗暴,所以風險很高。
reinterpret_cast 可以認爲是 static_cast 的一種補充,一些 static_cast 不能完成的轉換,就可以用 reinterpret_cast 來完成,例如兩個具體類型指針之間的轉換、int 和指針之間的轉換(有些編譯器只允許 int 轉指針,不允許反過來)。

具體用法如下:

#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* 轉換爲 float*
    char str[]="http://c.biancheng.net";
    float *p1 = reinterpret_cast<float*>(str);
    cout<<*p1<<endl;
    //將 int 轉換爲 int*
    int *p = reinterpret_cast<int*>(100);
    //將 A* 轉換爲 int*
    p = reinterpret_cast<int*>(new A(25, 96));
    cout<<*p<<endl;
   
    return 0;
}

運行結果:
3.0262e+29
25

【注】可以想象,用一個 float 指針來操作一個 char 數組是一件多麼荒誕和危險的事情,這樣的轉換方式不到萬不得已的時候不要使用。將 A* 轉換爲 int*,使用指針直接訪問 private 成員刺穿了一個類的封裝性,更好的辦法是讓類提供 get/set 函數,間接地訪問成員變量。

 

  • dynamic_cast 

dynamic_cast 用於在類的繼承層次之間進行類型轉換,它既允許向上轉型(Upcasting),也允許向下轉型(Downcasting)。向上轉型是無條件的,不會進行任何檢測,所以都能成功;向下轉型的前提必須是安全的,要藉助 RTTI 進行檢測,所有隻有一部分能成功。
dynamic_cast 與 static_cast 是相對的,dynamic_cast 是“動態轉換”的意思,static_cast 是“靜態轉換”的意思。dynamic_cast 會在程序運行期間藉助 RTTI 進行類型轉換,這就要求基類必須包含虛函數;static_cast 在編譯期間完成類型轉換,能夠更加及時地發現錯誤。

dynamic_cast 的語法格式爲:

dynamic_cast <newType> (expression)

newType 和 expression 必須同時指針類型或者引用類型。換句話說,dynamic_cast 只能轉換指針類型和引用類型,其它類型(int、double、數組、類、結構體等)都不行。

對於指針,如果轉換失敗將返回 NULL;對於引用,如果轉換失敗將拋出 std::bad_cast 異常。

Q&A

Q:爲什麼需要dynamic_cast強制轉換?
A:簡單的說,當無法使用virtual函數的時候

1) 向上轉型(Upcasting)

向上轉型時,只要待轉換的兩個類型之間存在繼承關係,並且基類包含了虛函數(這些信息在編譯期間就能確定),就一定能轉換成功。因爲向上轉型始終是安全的,所以 dynamic_cast 不會進行任何運行期間的檢查,這個時候的 dynamic_cast 和 static_cast 就沒有什麼區別了。

具體使用如下:

#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;  //輸出一個垃圾值
    pb2->func();  //內存錯誤
    return 0;
}

運行結果:

情況①是正確的,沒有任何問題。對於情況②,pd 指向的是整型變量 n,並沒有指向一個 Derived 類的對象,在使用 dynamic_cast 進行類型轉換時也沒有檢查這一點,而是將 pd 的值直接賦給了 pb(這裏並不需要調整偏移量),最終導致 pb 也指向了 n。因爲 pb 指向的不是一個對象,所以get_a()得不到 m_a 的值(實際上得到的是一個垃圾值),pb2->func()也得不到 func() 函數的正確地址( pb2->func() 得不到 func() 的正確地址的原因在於,pb2 指向的是一個假的“對象”,它沒有虛函數表,也沒有虛函數表指針,而 func() 是虛函數,必須到虛函數表中才能找到它的地址)。

2) 向下轉型(Downcasting)

向下轉型是有風險的,dynamic_cast 會藉助 RTTI 信息進行檢測,確定安全的才能轉換成功,否則就轉換失敗。那麼,哪些向下轉型是安全地呢,哪些又是不安全的呢?下面我們通過一個例子來演示:

#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);  //向下轉型失敗
    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);  //向下轉型失敗
    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();  //向上轉型都是允許的
    pb = dynamic_cast<B*>(pa);  //向下轉型成功
    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);  //向下轉型成功
    if(pc == NULL){
        cout<<"Downcasting failed: A* to C*"<<endl;
    }else{
        cout<<"Downcasting successfully: A* to C*"<<endl;
        pc -> func();
    }
   
    return 0;
}

運行結果:
Downcasting failed: A* to B*
Downcasting failed: A* to C*
-------------------------
Downcasting successfully: A* to B*
Class D
Downcasting successfully: A* to C*
Class D

這段代碼中類的繼承順序爲:A --> B --> C --> D。pa 是A*類型的指針,當 pa 指向 A 類型的對象時,向下轉型失敗,pa 不能轉換爲B*C*類型。當 pa 指向 D 類型的對象時,向下轉型成功,pa 可以轉換爲B*C*類型。同樣都是向下轉型,爲什麼 pa 指向的對象不同,轉換的結果就大相徑庭呢?
每個類都會在內存中保存一份類型信息,編譯器會將存在繼承關係的類的類型信息使用指針“連接”起來,從而形成一個繼承鏈(Inheritance Chain),也就是如下圖所示的樣子:

當使用 dynamic_cast 對指針進行類型轉換時,程序會先找到該指針指向的對象,再根據對象找到當前類(指針指向的對象所屬的類)的類型信息,並從此節點開始沿着繼承鏈向上遍歷,如果找到了要轉化的目標類型,那麼說明這種轉換是安全的,就能夠轉換成功,如果沒有找到要轉換的目標類型,那麼說明這種轉換存在較大的風險,就不能轉換。

對於本例中的情況①,pa 指向 A 類對象,根據該對象找到的就是 A 的類型信息,當程序從這個節點開始向上遍歷時,發現 A 的上方沒有要轉換的 B 類型或 C 類型(實際上 A 的上方沒有任何類型了),所以就轉換敗了。對於情況②,pa 指向 D 類對象,根據該對象找到的就是 D 的類型信息,程序從這個節點向上遍歷的過程中,發現了 C 類型和 B 類型,所以就轉換成功了。

總起來說,dynamic_cast 會在程序運行過程中遍歷繼承鏈,如果途中遇到了要轉換的目標類型,那麼就能夠轉換成功,如果直到繼承鏈的頂點(最頂層的基類)還沒有遇到要轉換的目標類型,那麼就轉換失敗。對於同一個指針(例如 pa),它指向的對象不同,會導致遍歷繼承鏈的起點不一樣,途中能夠匹配到的類型也不一樣,所以相同的類型轉換產生了不同的結果。

從表面上看起來 dynamic_cast 確實能夠向下轉型,本例也很好地證明了這一點:B 和 C 都是 A 的派生類,我們成功地將 pa 從 A 類型指針轉換成了 B 和 C 類型指針。但是從本質上講,dynamic_cast 還是隻允許向上轉型,因爲它只會向上遍歷繼承鏈。造成這種假象的根本原因在於,派生類對象可以用任何一個基類的指針指向它,這樣做始終是安全的。本例中的情況②,pa 指向的對象是 D 類型的,pa、pb、pc 都是 D 的基類的指針,所以它們都可以指向 D 類型的對象,dynamic_cast 只是讓不同的基類指針指向同一個派生類對象罷了。

 

  • 參考

【C++專題】static_cast, dynamic_cast, const_cast探討

C++四種類型轉換運算符:static_cast、dynamic_cast、const_cast和reinterpret_cast

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章