1. 程式人生 > >c++轉換static_cast 和 reinterpret_cast

c++轉換static_cast 和 reinterpret_cast

<<static_cast 和 reinterpret_cast>>

作者: 闕榮文([email protected])

C/C++是強型別語言,不同型別之間的相互轉換是比較麻煩的.但是在程式設計實踐中,不可避免的要用到型別轉換.有2中型別轉換:隱式型別轉換和強制型別轉換.


1.隱式型別轉換
1.1 提升精度,此種是編譯器自動完成的,安全的.所以編譯的時候不會有任何錯誤或者警告資訊提示.
示例: <<C++ Primer (第三版)>> P147
int ival = 3;
double dval = 3.14159;

// ival 被提升為 double 型別: 3.0
ival + dval;

1.2 降低精度,也是有編譯器自動完成,會造成精度丟失,所以編譯時得到一個警告資訊提示.
示例:
double dval = 3.14159;
// dval的值被擷取為 int 值3
int ival = dval;

2.顯式型別轉換

2.1 C風格的強制轉換(包括舊式C++風格的強制轉換)

格式:
型別(表示式); // 舊的C++風格
或者
(型別)表示式 // C風格

示例: int(dval) 或者 (int)dval

此種強制轉換是比較粗暴直接的,有可能導致精度丟失(如從 double 轉換為 int)或者一些莫名其妙的錯誤(如把 int 轉換為 函式指標),一旦使用了強制轉換,編譯器將不提示任何警告.這也往往成為錯誤的源泉.而且這種錯誤非常難找.我想這也是C++要使用新的強制轉換操作符的原因之一吧.

2.2 C++強制轉換操作符
C++增加了4個關鍵字用於強制型別轉換:
static_cast, reinterpret_cast, const_cast 和 dynamic_cast.

const_cast 用來移除 const,這個沒什麼好說的.
dynamic_cast 需要 RTTI 支援, 主要用於把基類指標轉換為派生類指標.這裡的基類指標其實是指向一個派生類例項,只是型別為基類.
示例:
// 前提假設: class B 由 class A 派生
A *ptrA = new class B;
B *ptrB = dynamic_cast<B*>(ptrA);

本文主要談談 static_cast 和 reinterpret_cast 的用法和區別.
<<C++程式程式設計語言>>裡有一句話我認為說到點子上了: static_cast 運算子完成*相關型別*之間的轉換. 而 reinterpret_cast 處理*互不相關的型別*之間的轉換.

所謂"相關型別"指的是從邏輯上來說,多多少少還有那麼一點聯絡的型別,比如從 double 到 int,我們知道它們之間還是有聯絡的,只是精度差異而已,使用 static_cast 就是告訴編譯器:我知道會引起精度損失,但是我不在乎. 又如從 void* 到 具體型別指標像 char*,從語義上我們知道 void* 可以是任意型別的指標,當然也有可能是 char* 型的指標,這就是所謂的"多多少少還有那麼一點聯絡"的意思. 又如從派生類層次中的上行轉換(即從派生類指標到基類指標,因為是安全的,所以可以用隱式型別轉換)或者下行轉換(不安全,應該用 dynamic_cast 代替).
對於static_cast操作符,如果需要截斷,補齊或者指標偏移編譯器都會自動完成.注意這一點,是和 reinterpret_cast 的一個根本區別.

"互不相關的型別"指的是兩種完全不同的型別,如從整型到指標型別,或者從一個指標到另一個毫不相干的指標.
示例:
int ival = 1;
double *dptr = reinterpret_cast<double*>(ival);

或者
int *iptr = NULL;
double *dptr = reinterpret_cast<double*>(iptr);

reinterpret_cast 操作執行的是位元位拷貝,就好像用 memcpy() 一樣.

int *iptr = reinterpret_cast<int*>(1);
double *dptr = reinterpret_cast<double*>(2);
memcpy(&dptr, &iptr, sizeof(double*)); // 等效於 dptr = reinterpret_cast<double*>(iptr); 結果 dptr 的值為1;

上面這個示例也說明了 reinterpret_cast 的意思:編譯器不會做任何檢查,截斷,補齊的操作,只是把位元位拷貝過去.
所以 reinterpret_cast 常常被用作不同型別指標間的相互轉換,因為所有型別的指標的長度都是一致的(32位系統上都是4位元組),按位元位拷貝後不會損失資料.

3. 程式設計實踐中幾種典型的應用場景


3.1 數值精度提示或者降低,包括把無符號型轉換為帶符號型(也是精度損失的一種),用 static_cast 可以消除編譯器的警告資訊,前面提到好幾次了.

3.2 任意型別指標到 void*, 隱式型別轉換,自動完成. 看看 memcpy 的原型
void *memcpy(
   void *dest,
   const void *src,
   size_t count
);
引數定義為 void* 是有道理的,不管我們傳入什麼型別的指標都符合語義,並且不會有編譯器警告.

3.3 void* 到任意型別指標, 用 static_cast 和 reinterpret_cast 都可以,這是由 void* 是通用指標這個語義決定的.我個人傾向用 reinterpret_cast,表達要"重新解釋"指標的語義.

3.4 不同型別指標間的相互轉換用 reinterpret_cast.

3.5 int 型和指標型別間的相互轉換用 reinterpret_cast.
比如我寫程式碼的時候經常這樣做: new 一個 struct,然後把指標返回給外部函式作為一個"控制代碼",我不希望外部函式知道這是一個指標,只需要外部函式在呼叫相關函式時把這個"控制代碼"重新傳回來.這時,就可以把指標轉換為一個 int 型返回. 這是 reinterpret_cast 存在的絕佳理由.

struct car
{
    int doors;
    int height;
    int length;
    float weight;
};

int create_car()
{
    car *c = new car;
    return reinterpret_cast<int>(c);
}

int get_car_doors(int car_id)
{
    car *c = reinterpret_cast<car*>(car_id);
    return c->doors;
}

void destroy_car(int car_id)
{
    car *c = reinterpret_cast<car*>(car_id);
    delete c;
}

如上,外部函式不需要知道 struct car 的具體定義,只需要呼叫 create_car() 得到一個 car id,然後用此 car_id 呼叫其他相關函式即可,至於 car_id 是什麼,根本沒必要關心.

3.6 派生類指標和基類指標間的相互轉換.
3.6.1 派生類指標到基類指標用隱式型別轉換(直接賦值)或者用 static_cast. 顯然不應該也沒必要用 reinterpret_cast.
3.6.2 基類指標到派生類指標用 dynamic_cast (執行期檢查)或者 static_cast (執行期不檢查,由程式設計師保證正確性). 考慮到C++物件模型的記憶體分佈可能引起的指標偏移問題,絕對不能用 reinterpret_cast.

後記

幾乎所有提到 reinterpret_cast 的書籍都要附帶說什麼"不可移植","危險"之類的詞,好像 reinterpret_cast 是洪水猛獸,碰不得摸不得.其實理解了之後就知道沒什麼神祕的,存在即是理由,該用的時候就要大膽的用,否則C++保留這個關鍵字幹什麼? 關鍵是程式設計師應該清楚的知道自己要的結果是什麼,如此,就是用C風格的強制轉換又有何妨?