1. 程式人生 > 實用技巧 >C++ 型別轉換的實現

C++ 型別轉換的實現

【前言】
C++型別轉換主要分為兩種:隱式型別轉換、顯式型別轉換(強制型別轉換)。隱式型別轉換主要是通過operator(型別轉換函式)來實現的;強制型別轉換主要用到四個關鍵字,下面部落格內容就按隱式→顯式四個關鍵字 來展開。


一、隱式型別轉換

1 基本資料型別間

基本資料型別之間進行隱式轉換的幾種情況:

1、算術轉換(Arithmetic conversion) : 在混合型別的 算術表示式 中, 位數最寬的資料型別成為目標轉換型別。例如:

int i=3;
double j = 3.1;
i+j;//i會被轉換成double型別,然後才做加法運算。

2、一種型別表示式賦值給另一種型別的物件:目標型別是被賦值物件的型別。例如:

int inum = -1;
unsigned int uinum = 1;
double dnum = 2.3;
uinum = inum; //4294967295
inum = dnum;  //2

這種情況的轉換,窄型別轉換為寬型別一般沒有問題,寬型別轉換為窄型別一般會截斷轉換,有符號型別轉換為無符號型別會出現我們不想出現的情況。

3、將一個表示式作為實參傳遞給函式呼叫,此時形參和實參型別不一致:目標轉換型別為形參的型別。例如:

extern double sqrt( double );
cout << " The square root of 2 is  " << sqrt(2) << endl;
// 2被提升為double型別:2.0

4、從一個函式返回一個表示式,表示式型別與返回型別不一致:目標轉換型別為函式的返回型別。例如:

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

2 使用者自定義資料型別間

使用子類物件代替父類物件是可以的,也是因為隱式型別轉換:

class A{};
class B: public A
{};//B是子類
void Fun(A& a);
B b;
Fun(b);//使用子類物件代替父類物件是可以的,也是因為隱式型別轉換。

如果想將當前類型別轉換為其他型別的時候,該怎麼做呢?C++ 提供了型別轉換函式(Type conversion function)來解決這個問題,它只能以成員函式的形式出現,也就是隻能出現在類中。型別轉換函式的語法格式為:

operator type(){
    //TODO:
    return data;
}

operator 是 C++ 關鍵字,type 是要轉換的目標型別,data 是要返回的 type 型別的資料。

因為要轉換的目標型別是 type,所以返回值 data 也必須是 type 型別。既然已經知道了要返回 type 型別的資料,所以沒有必要再像普通函式一樣明確地給出返回值型別。這樣做導致的結果是:型別轉換函式看起來沒有返回值型別,其實是隱式地指明瞭返回值型別。

型別轉換函式也沒有引數,因為要將當前類的物件轉換為其它型別,所以引數不言而喻。實際上編譯器會把當前物件的地址賦值給 this 指標,這樣在函式體內就可以操作當前物件了。

型別轉換函式可以看成式轉換建構函式的逆,因為轉換建構函式能夠將其它型別轉換為當前類型別,而型別轉換函式可以將當前類型別轉換為其他類型別。下面看個例子:

//複數類
class Complex{
public:
    Complex(): m_real(0.0), m_imag(0.0){ }
    Complex(double real, double imag): m_real(real), m_imag(imag){ }
public:
    friend ostream & operator<<(ostream &out, Complex &c);
    friend Complex operator+(const Complex &c1, const Complex &c2);
    operator double() const { return m_real; }  //型別轉換函式
private:
    double m_real;  //實部
    double m_imag;  //虛部
};

裡面operator那一行,就是型別轉換函式,它可以將複數型別轉換為double型別,有了它,下面的程式碼就可以成功運行了:

Complex c1(24.6, 100);
double f = c1;  //相當於 double f = Complex::operator double(&c1);

二、顯式型別轉換

筆者認為C++是對C補充和更新,首先看C的強制轉換:

(type-id)expression//轉換格式1
type-id(expression)//轉換格式2

c語言強制型別轉換主要用於 基礎的資料型別間的轉換,c++除了能使用c語言的強制型別轉換外,還新增了四種強制型別轉換:static_cast、dynamic_cast、const_cast、reinterpret_cast主要運用於繼承關係類間的強制轉化

static_cast<new_type>      (expression)
dynamic_cast<new_type>     (expression) 
const_cast<new_type>       (expression) 
reinterpret_cast<new_type> (expression)

new_type為目標資料型別,expression為原始資料型別變數或者表示式。對其簡單如下表所示:

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

有的書中,將c語言強制型別轉換稱為舊式轉型,c++強制型別轉換稱為新式轉型。

1 static_cast

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 和指標之間的轉換。將一個具體的地址賦值給指標變數是非常危險的,因為該地址上的記憶體可能沒有分配,也可能沒有讀寫許可權,恰好是可用記憶體反而是小概率事件。
  • static_cast 也不能用來去掉表示式的 const 修飾和 volatile 修飾。換句話說,不能將 const/volatile 型別轉換為非 const/volatile 型別。

下面是一些用法示例:

//下面是正確的用法
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);  //不能將整數轉換為指標型別

2 const_cast

const_cast 比較好理解,它用來去掉表示式的 const 修飾或 volatile 修飾。換句話說,const_cast 就是用來將 const/volatile 型別轉換為非 const/volatile 型別。看下面的例子:

const int n = 100;
int *p = const_cast<int*>(&n);
*p = 234;
cout<<"n = "<<n<<endl;   //  n = 100
cout<<"*p = "<<*p<<endl; //  *p = 234

&n用來獲取 n 的地址,它的型別為const int *,必須使用 const_cast 轉換為int *型別後才能賦值給 p。由於 p 指向了 n,並且 n 佔用的是棧記憶體,有寫入許可權,所以可以通過 p 修改 n 的值。也就是說:p 和&n指向的是相同的地址。

有讀者可能會問,為什麼通過 n 和 *p 輸出的值不一樣呢?這是因為 C++ 對常量的處理更像是編譯時期的#define,是一個值替換的過程,程式碼中所有使用 n 的地方在編譯期間就被替換成了 100。換句話說,cout<<"n = "<<n<<endl;這行程式碼被修改成了cout<<"n = "<<100<<endl;

這樣以來,即使程式在執行期間修改 n 的值,也不會影響 cout 語句了。使用 const_cast 進行強制型別轉換可以突破 C/C++ 的常數限制,修改常數的值,因此有一定的危險性;但是程式設計師如果這樣做的話,基本上會意識到這個問題,因此也還有一定的安全性。

3 reinterpret_cast

reinterpret 是“重新解釋”的意思,顧名思義,reinterpret_cast 這種轉換僅僅是對二進位制位的重新解釋,不會藉助已有的轉換規則對資料進行調整,非常簡單粗暴,所以風險很高。

reinterpret_cast 可以認為是 static_cast 的一種補充,一些 static_cast 不能完成的轉換,就可以用 reinterpret_cast 來完成,例如兩個具體型別指標之間的轉換、int 和指標之間的轉換(有些編譯器只允許 int 轉指標,不允許反過來)。

下面的程式碼程式碼演示了 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* 轉換為 float*
    char str[]="http://c.biancheng.net";
    float *p1 = reinterpret_cast<float*>(str);
    cout<<*p1<<endl;  //3.0262e+29
    //將 int 轉換為 int*
    int *p = reinterpret_cast<int*>(100);
    //將 A* 轉換為 int*
    p = reinterpret_cast<int*>(new A(25, 96));
    cout<<*p<<endl; //25
    return 0;
}

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

4 dynamic_cast

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

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

dynamic_cast <newType> (expression)

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

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

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<Base*>(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語言中文網
菜鳥教程
部落格園
CSDN