首頁 > 軟體

C++強制型別轉換的四種方式

2022-05-24 22:05:05

1 C++類別型轉換本質

1.1 自動型別轉換(隱式)

利用編譯器內建的轉換規則,或者使用者自定義的轉換建構函式以及型別轉換函數(這些都可以認為是已知的轉換規則)。
例如從 int 到 double、從派生類到基礎類別、從type *到void *、從 double 到 Complex 等。
注:

type *是一個具體型別的指標,例如int *、double *、Student *等,它們都可以直接賦值給void *指標。
例如,malloc() 分配記憶體後返回的就是一個void *指標,我們必須進行強制型別轉換後才能賦值給指標變數。

1.2 強制型別轉換(顯式)

隱式不能完成的型別轉換工作,就必須使用強制型別轉換
(new_type) expression

1.3 型別轉換的本質

資料型別的本質:
這種「確定資料的解釋方式」的工作就是由資料型別(Data Type)來完成的。例如int a;表明,a 這份資料是整數,不能理解為畫素、聲音、視訊等。
資料型別轉換的本質:
資料型別轉換,就是對資料所佔用的二進位制位做出重新解釋。

  • 隱式型別轉換:編譯器可以根據已知的轉換規則來決定是否需要修改資料的二進位制位
  • 強制型別轉換:由於沒有對應的轉換規則,所以能做的事情僅僅是重新解釋資料的二進位制位,但無法對資料的二進位制位做出修正。

1.4 型別轉換的安全性

隱式型別轉換必須使用已知的轉換規則,雖然靈活性受到了限制,但是由於能夠對資料進行恰當地調整,所以更加安全(幾乎沒有風險)。
強制型別轉換能夠在更大範圍的資料型別之間進行轉換,例如不同型別指標(參照)之間的轉換、從 const 到非 const 的轉換、從 int 到指標的轉換(有些編譯器也允許反過來)等,這雖然增加了靈活性,但是由於不能恰當地調整資料,所以也充滿了風險,程式設計師要小心使用。

2 四種型別轉換運運算元

2.1 C語言的強制型別轉換與C++的區別

C風格的強制型別轉換統一使用(),而()在程式碼中隨處可見,所以也不利於使用檢索工具定位強轉的程式碼位置。
C++ 對型別轉換進行了分類,並新增了四個關鍵字來予以支援,它們分別是:

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

語法格式為: xxx_cast<newType>(data)

3 static_cast

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

4 reinterpret_cast

reinterpret_cast 用於進行各種不同型別的指標之間、不同型別的參照之間以及指標和能容納指標的整數型別之間的轉換。轉換時,執行的是逐個位元複製的操作。

#include <iostream>
using namespace std;
class A
{
public:
    int i;
    int j;
    A(int n):i(n),j(n) { }
};
int main()
{
    A a(100);
    int &r = reinterpret_cast<int&>(a); //強行讓 r 參照 a
    r = 200;  //把 a.i 變成了 200
    cout << a.i << "," << a.j << endl;  // 輸出 200,100
    int n = 300;
    A *pa = reinterpret_cast<A*> ( & n); //強行讓 pa 指向 n
    pa->i = 400;  // n 變成 400
    pa->j = 500;  //此條語句不安全,很可能導致程式崩潰
    cout << n << endl;  // 輸出 400
    long long la = 0x12345678abcdLL;
    pa = reinterpret_cast<A*>(la); //la太長,只取低32位元0x5678abcd拷貝給pa
    unsigned int u = reinterpret_cast<unsigned int>(pa);//pa逐個位元拷貝到u
    cout << hex << u << endl;  //輸出 5678abcd
    typedef void (* PF1) (int);
    typedef int (* PF2) (int,char *);
    PF1 pf1;  PF2 pf2;
    pf2 = reinterpret_cast<PF2>(pf1); //兩個不同型別的函數指標之間可以互相轉換
}

reinterpret_cast體現了 C++ 語言的設計思想:使用者可以做任何操作,但要為自己的行為負責。

5 const_cast

const_cast 運運算元僅用於進行去除 const 屬性的轉換,它也是四個強制型別轉換運運算元中唯一能夠去除 const 屬性的運運算元。

將 const 參照轉換為同型別的非 const 參照,將 const 指標轉換為同型別的非 const 指標時可以使用 const_cast 運運算元。例如:

const string s = "Inception";
string& p = const_cast <string&> (s);
string* ps = const_cast <string*> (&s);  // &s 的型別是 const string*

6 dynamic_cast

用 reinterpret_cast 可以將多型基礎類別(包含虛擬函式的基礎類別)的指標強制轉換為派生類的指標,但是這種轉換不檢查安全性,即不檢查轉換後的指標是否確實指向一個派生類物件。
dynamic_cast專門用於將多型基礎類別的指標或參照強制轉換為派生類的指標或參照,而且能夠檢查轉換的安全性。對於不安全的指標轉換,轉換結果返回 NULL 指標。

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

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

dynamic_cast 是通過“執行時型別檢查”來保證安全性的。
dynamic_cast 不能用於將非多型基礎類別的指標或參照強制轉換為派生類的指標或參照——這種轉換沒法保證安全性,只好用 reinterpret_cast 來完成。

6.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() 是虛擬函式,必須到虛擬函式表中才能找到它的地址。

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

當使用 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 只是讓不同的基礎類別指標指向同一個派生類物件罷了。

參考:http://c.biancheng.net/view/2343.html

到此這篇關於C++強制型別轉換的四種方式的文章就介紹到這了,更多相關C++強制型別轉換內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com