C++中dynamic_cast與static_cast淺析與例項演示

進擊的汪sir發表於2021-07-24

1. static_cast

1.1 static_cast語法

static_cast< new_type >(expression)

備註new_type為目標資料型別,expression為原始資料型別變數或者表示式。

C風格寫法:

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

C++ 新風格的寫法為:

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

1.2 為什麼要有static_cast等

隱式型別轉換是安全的,顯式型別轉換是有風險的,C語言之所以增加強制型別轉換的語法,就是為了強調風險,讓程式設計師意識到自己在做什麼。

但是,這種強調風險的方式還是比較粗放,粒度比較大,它並沒有表明存在什麼風險,風險程度如何。

為了使潛在風險更加細化,使問題追溯更加方便,使書寫格式更加規範,C++ 對型別轉換進行了分類,並新增了四個關鍵字來予以支援,它們分別是:

關鍵字 說明
static_cast 用於良性轉換,一般不會導致意外發生,風險很低。
const_cast 用於 const 與非 const、volatile 與非 volatile 之間的轉換。
reinterpret_cast 高度危險的轉換,這種轉換僅僅是對二進位制位的重新解釋,不會藉助已有的轉換規則對資料進行調整,但是可以實現最靈活的 C++ 型別轉換。
dynamic_cast 藉助 RTTI,用於型別安全的向下轉型(Downcasting)。

1.2 static_cast的作用

static_cast相當於傳統的C語言裡的強制轉換,該運算子把expression轉換為new_type型別,用來強迫隱式轉換如non-const物件轉為const物件,編譯時檢查,用於非多型的轉換,可以轉換指標及其他,但沒有執行時型別檢查來保證轉換的安全性。它主要有如下幾種用法:

風險較低的用法:

  • 原有的自動型別轉換,例如 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.3 static_cast用法

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

2. dynamic_cast

2.1 dynamic_cast 語法

dynamic_cast <newType> (expression)

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

對於指標,如果轉換失敗將返回 NULL;對於引用,如果轉換失敗將丟擲std::bad_cast異常。

2.2 dynamic_cast 用法

dynamic_cast 用於在類的繼承層次之間進行型別轉換,它既允許向上轉型(Upcasting),也允許向下轉型(Downcasting)。向上轉型是無條件的,不會進行任何檢測,所以都能成功;向下轉型的前提必須是安全的,要藉助 RTTI 進行檢測,所有隻有一部分能成功。

dynamic_cast 與 static_cast 是相對的,dynamic_cast 是“動態轉換”的意思,static_cast 是“靜態轉換”的意思。dynamic_cast 會在程式執行期間藉助 RTTI 進行型別轉換,這就要求基類必須包含虛擬函式;static_cast 在編譯期間完成型別轉換,能夠更加及時地發現錯誤。

2.3 dynamic_cast 例項

2.3.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;
}

執行結果如下

image-20210724114624100

可以看到pd1與pb1的地址相同,且pb1可以正常呼叫Base類的方法

對於情況②

pd 2指向的是整型變數 n,並沒有指向一個 Derived 類的物件,在使用 dynamic_cast 進行型別轉換時也沒有檢查這一點因為向上轉型始終是安全的,所以 dynamic_cast 不會進行任何執行期間的檢查

而是將 pd 的值直接賦給了 pb(這裡並不需要調整偏移量),最終導致 pb 也指向了 n。因為 pb 指向的不是一個物件,所以get_a()得不到 m_a 的值(實際上得到的是一個垃圾值),pb2->func()得不到 func() 函式的正確地址

執行結果如下

image-20210724115237948

image-20210724115309768

簡單來說就是向上轉型是不檢查的,所以大家得知道自己在做什麼,不能隨意的轉換

2.3.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;
}

執行結果

image-20210724120141187

可以看到,前兩次轉換失敗,但是後兩次轉換成功

這段程式碼中類的繼承順序為:A --> B --> C --> D。pa 是A*型別的指標,當 pa 指向 A 型別的物件時,向下轉型失敗,pa 不能轉換為B*C*型別。當 pa 指向 D 型別的物件時,向下轉型成功,pa 可以轉換為B*C*型別。同樣都是向下轉型,為什麼 pa 指向的物件不同,轉換的結果就大相徑庭呢?

因為每個類都會在記憶體中儲存一份型別資訊,編譯器會將存在繼承關係的類的型別資訊使用指標“連線”起來,從而形成一個繼承鏈(Inheritance Chain),也就是如下圖所示的樣子:

image-20210724120354068

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

所以在第二種方式中,pa實際上是指向的D,於是程式順著D開始向上找,找到了B和C,於是認定是安全的,所以轉換成功

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

3. 參考連結

http://c.biancheng.net/cpp/biancheng/view/3297.html

https://blog.csdn.net/u014624623/article/details/79837849
https://www.cnblogs.com/wanghongyang/ 【本文部落格】

相關文章