C++類型轉換

一篇來自cplusplus.com的文章,這是我所看過的關於C++類型轉換的最全面、最仔細、最深刻的一篇文章。本文介紹了C++的各類類型轉換,詳細包含:基本類型的隱式類型轉換,C風格的類型轉換,類的隱式轉換(implicit conversion),explicitkeyword,static_cast, reintperet_cast, const_cast, dynamic_cast。 以及和RTTI相關的typeidkeyword。
java

原文連接(英文):http://www.cplusplus.com/doc/tutorial/typecasting/
ios


隱式類型轉換

(ps:首先是內置類型之間的類型轉換)express

當一個值被複制到一個可與之兼容的類型時,隱式轉化本身主動發生。好比:安全

short a = 2000;
int b;
b = a;
這裏,a的值從short提高到了int不需要不論什麼顯式的運算符,這被稱做標準轉換(standard conversion)。標準轉換做用於基本數據類型,贊成數值類型之間的轉換(short 到int, int 到float,double到int...), 轉或從bool,以及一些指針轉換。

從較小的整型類型轉到int,或者從float轉到double被稱做提高(promotion),在目標類型中產生絕對一樣的值能夠獲得保證。其它算術類型之間的轉型不必定能夠表示絕對一樣的值:函數

  • 假設從一個負整數(PS:有符號)轉換到無符號類型,獲得的值相應於它的二進制補數表示
  • 從(到)bool值轉型,false將等價於0(對於數值類型),或者空指針(null pointer)(對於指針類型);所有其它值等價於true, 且會被轉換爲1
  • 假設是從浮點型到整型的轉換,值將被截斷(小數部分將會丟失)。假設結果超出了(目標)類型的表示範圍,將會致使沒有定義的行爲(undefined behavior)
  • 另外,假設是一樣類型的數值類型之間的轉換(整型到整型,或浮點到浮點),轉型是合法的,但值是實現定義的(implementation-specific,PS:編譯器實現的),且可能會致使不可移植.(ps: 沒人會這麼幹)

以上這些轉型有可能致使精度丟失,這些狀況編譯器能夠產生警告。這些警告能夠經過顯式轉換避免。this

對於非基本類型,數值和函數能隱式轉換爲指針,而指針一般贊成例如如下轉換:spa

  • NULL可以轉到隨意類型的指針
  • 隨意類型的指針可以轉換void*
  • 指針向上轉型:派生類指針可以轉換到一個可以訪問的(accessible)且非不明白的(unambiguous)基類指針,不會丟失它的const或volatile限定。

類間隱式轉換

在類的世界裏,隱式轉換可以經過下面三種成員函數控制:翻譯

  • 單參數構造函數(Single-argument constructors): 贊成從一個已知類型的隱式轉換,用來初始化一個對象
  • 賦值運算符(Assignment operator): 贊成從一個已知類型的隱式轉換,用來賦值
  • 類型轉換運算符(Type-cast opertor, PS:也叫operator type): 贊成轉到一個已知類型

好比:指針

// implicit conversion of classes:
#include <iostream>
using namespace std;

class A {};

class B {
public:
  // conversion from A (constructor):
  B (const A& x) {}
  // conversion from A (assignment):
  B& operator= (const A& x) {return *this;}
  // conversion to A (type-cast operator)
  operator A() {return A();}
};

int main ()
{
  A foo;
  B bar = foo;    // calls constructor
  bar = foo;      // calls assignment
  foo = bar;      // calls type-cast operator
  return 0;
}

類型轉換運算符用一個已知的語句:用一個opertorkeyword跟目標類型和一個空的圓括號。記住:返回值類型是目標類型,且它並無在operatorkeyword的前面指出。(PS:通常的運算符重載成員函數,返回值類型是寫在operator前面的)code


explicitkeyword

在一個函數調用上,C++贊成每個參數的隱式轉換。這對於類可能會有點問題,因爲它並不老是指望的。好比,咱們將如下的函數加到剛纔的樣例中:

void fn (B arg) {}

這個函數有個類型爲B的參數,但它也能以一個A類型的參數進行調用:

fn(foo);

這多是不指望的。但是,不論什麼狀況下,能夠經過在構造函數上使用explicitkeyword獲得保護:

// explicit:
#include <iostream>
using namespace std;

class A {};

class B {
public:
  explicit B (const A& x) {}
  B& operator= (const A& x) {return *this;}
  operator A() {return A();}
};

void fn (B x) {}

int main ()
{
  A foo;
  B bar (foo);
  bar = foo;
  foo = bar;
  
//  fn (foo);  // not allowed for explicit ctor.
  fn (bar);  

  return 0;
}

另外,標上explicitkeyword的構造函數不能使用賦值式的語句進行調用;這樣的狀況下的樣例是,bar不能想如下這樣構造:

B bar = foo;

類型轉換成員函數(前面章節以及描寫敘述)也能夠指定爲explicit. 這相同能夠像保護explicit修飾的ctor那樣保護到目標類型的轉換。


類型轉換

C++是一種強類型的語言。多數轉型,特別是那些用在值的表示形式不一樣的(類型之間的轉換),需要顯式轉換,在C++中稱做類型轉換(type-casting)。有兩種通用的用於類型轉換的語句:函數式的(functional)和C風格的(c-like):

double x = 10.3;
int y;
y = int (x);    // functional notation
y = (int) x;    // c-like cast notation 

對於多數的基本類型,函數式這樣的通用的類型轉換形式已經足夠。然而,這樣的形式能夠被任意地應用在類和指向類的指針上,這會致使代碼——語法上正確,卻有執行時錯誤。好比,如下的代碼能夠沒有錯誤的編譯:

// class type-casting
#include <iostream>
using namespace std;

class Dummy {
    double i,j;
};

class Addition {
    int x,y;
  public:
    Addition (int a, int b) { x=a; y=b; }
    int result() { return x+y;}
};

int main () {
  Dummy d;
  Addition * padd;
  padd = (Addition*) &d;
  cout << padd->result();
  return 0;
}

這個程序聲明瞭一個指向Addition的指針,但隨後被用顯式轉換賦值爲一個不相關的類型:

padd = (Addition*) &d;
沒有限制的顯示類型轉換贊成隨意指針轉爲其它類型的指針, 獨立於她們實際指向的類型。接下來的成員函數調用將會致使一個執行時錯誤或者一些不但願的結果。


新式轉型運算符

爲了控制這樣的類之間的轉型,咱們有四種特定的類型轉換運算符:dynamic_cast, reinterpret_cast, static_cast, 和 const_cast. 它們的格式是新類型被尖括號(<>)括起來,跟在後面的是括號括起來的要轉型的表達式。

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

傳統的類型轉換表達式的是:

(new_type) expression
new_type (expression)

但是它們每個都有特色:

static_cast

static_cast可以進行相關類之間的指針轉換,不但可以向上轉型(從派生類指針轉向基類指針),而且可以向下轉型(從基類指針到派生類指針)。執行時沒有檢查以保證對象已轉化爲一個完整的目標類型。所以,程序猿有責任保證轉換的安全性。還有一方面,它也不會像dynamic_cast那樣添加類型安全檢查的開銷。

class Base {};
class Derived: public Base {};
Base * a = new Base;
Derived * b = static_cast<Derived*>(a);

這將會是合法的代碼,雖然b指向一個不完整的對象且解引用時可能致使執行時錯誤。

所以,static_cast不但能夠進行隱式的轉型(PS: 如upcast, 整型轉換),而且能夠進行相反的轉型(PS: 如downcast,但是最好別這麼幹!)。

static_cast能夠進行所有的隱式轉型贊成的轉型(不止是樣例中的指向類的指針之間),而且能夠進行和它相反的轉型(PS: 這句反覆了!)。它能夠:

  • 從void*轉換到隨意類型的指針。這樣的狀況下,它會保證假設void*值能夠被目標指針值容納,結果指針值將會是一樣的
  • 轉換整型,浮點值,以及枚舉類型到枚舉類型

另外,static_cast能夠進行如下的:

  • 明白地調用單參數的構造函數(single-argument constructor)或一個轉型運算符(conversion operator)
  • 轉換到右值引用(rvalue reference, PS: C++11)
  • 轉換枚舉值到整型或浮點值
  • 轉換不論什麼類型到void,計算並忽略這個值

(PS: static_cast可以替換絕大多數的C風格的基本類型轉換)


reinterpret_cast

reinterpret_cast轉換隨意指針類型到其它隨意指針類型,即便是不相關的類。這樣的操做的結果是僅僅簡單的二進制的將值從一個指針複製到還有一個。所有的指針轉型都是贊成的:它自己的指針類型和所指內容都不會被檢查。

它相同能夠轉換指針類型到整型類型,或從整型轉換到指針。這個整型值所表示的指針的格式是平臺相關的(platform-specific, PS:可能和大小段有關)。僅有的保證是一個指針轉換到一個整型是足夠全然容納它的(比方 intptr_t), 保證能夠轉換回一個合法的指針。

可使用reinterpret_cast卻不能用static_cast的轉型是基於重解釋(reinterperting)類型的二進制表示的低級操做,這多數狀況下,代碼的結果是系統相關(system-sepcific),而且不可移植(non-portable)。好比:

class A { /* ... */ };
class B { /* ... */ };
A * a = new A;
B * b = reinterpret_cast<B*>(a);

這段代碼編譯起來並無多少感受,但是今後之後,b指向了一個全然不相關的且不是完整的B類的對象a,解引用b是不安全的。

(PS: reinterpret_cast可以替換多數C風格的指針轉換)


const_cast

這樣的類型的轉換用來操做指針所指對象的const屬性,包含設置和去除。好比,傳遞一個const指針到一個指望non-cast參數的函數:

// const_cast
#include <iostream>
using namespace std;

void print (char * str)
{
  cout << str << '\n';
}

int main () {
  const char * c = "sample text";
  print ( const_cast<char *> (c) );
  return 0;
}
程序輸出:sampel text

這個樣例保證能夠執行是因爲函數僅僅是打印,並無改動它所指的對象。相同記住:去除所指對象的const屬性,再改動它會致使未定義的行爲(undefined behavior, PS: 標準未定義的行爲)。

dynamic_cast

dynamic_cast僅僅能用於類的指針和引用(或者是void*). 它的目的是保證轉型的結果指向一個目標類型的可用的完整對象。

這天然包含指針的向上轉型(pointer upcast, 從派生類指針轉向基類指針), 相同的方法也相同贊成隱式轉換。

但dynamic_cast還可以向下轉型(downcast, 從基類指針轉向派生類指針)多態的類(具備virtual成員的),當且僅當,所指對象是一個合法的完整的目標類型對象(轉型纔會成功)。好比:

// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;

class Base { virtual void dummy() {} };
class Derived: public Base { int a; };

int main () {
  try {
    Base * pba = new Derived;
    Base * pbb = new Base;
    Derived * pd;

    pd = dynamic_cast<Derived*>(pba);
    if (pd==0) cout << "Null pointer on first type-cast.\n";

    pd = dynamic_cast<Derived*>(pbb);
    if (pd==0) cout << "Null pointer on second type-cast.\n";

  } catch (exception& e) {cout << "Exception: " << e.what();}
  return 0;
}
程序輸出:Null pointer on second type-cast.

通用性提示:這樣的類型的dynamic_cast需要 執行時類型識別(Run-Time Type Information)以保持對動態類型的跟蹤。一些編譯器支持這個特性,但默認倒是關閉的。這時,需要開啓執行時類型檢查才幹使dynamic_cast執行良好。

接下來的代碼,試圖進行兩次從Base*類型的指針到Derived*類型的指針的轉型,但僅僅有第一次是成功的。看清它們各自的初始化:

Base * pba = new Derived;
Base * pbb = new Base;

雖然兩個指針都是Base*類型的指針,pba實際指向的對象的類型是Derived,而pbb指向一個Base類型的對象。所以,當它們分別用dynamic_cast進行類型轉換時,pba指向的是一個完整的Derived類的對象,而pbb指向的是一個Base類型的對象,不是一個完整的Derived類型。

當dynamic_cast(表達式內)指針不是一個完整的所需對象類型時(上例中的第二次轉型),它將獲得一個空指針(null pointer)以表示轉型失敗(PS: 這是dynamic_cast的獨特之處)。假設dynamic_cast用來轉換一個引用類型,且轉換是不可能的,對應的將會拋出bad_cast異常。

dynamic_cast還贊成進行指針的其它隱式轉換:指針類型之間轉換空指針(即便是不相關的類),轉換隨意指針類型到void*指針。(PS:不太常用)

(PS: dynamic_cast的RTTI能力是C-like作不到的,它的downcast做用至關於java的isinstanceof)


typeid

typeid贊成用來檢查一個表達式的類型:

typeid (expression)

這個操做符將返回一個定義在標準頭文件<typeinfo>中的type_info類型的常量對象(constant object)的引用。一個typeid返回的值可以用==運算符和!=運算符與還有一個typeid返回的值進行比較,或者可以經過用它的name()成員獲得一個字符串(null-terminated character sequence)表示他的數據類型或類名。

// typeid
#include <iostream>
#include <typeinfo>
using namespace std;

int main () {
  int * a,b;
  a=0; b=0;
  if (typeid(a) != typeid(b))
  {
    cout << "a and b are of different types:\n";
    cout << "a is: " << typeid(a).name() << '\n';
    cout << "b is: " << typeid(b).name() << '\n';
  }
  return 0;
}
程序輸出:

a and b are of different types:
a is: int *
b is: int  

當typeid被用在類上,typeid使用RTTI來跟蹤動態類型對象(PS: 有virtual function的)。當typeid被用於一個多態class類型的表達式,結果將是完整的派生對象:

// typeid, polymorphic class
#include <iostream>
#include <typeinfo>
#include <exception>
using namespace std;

class Base { virtual void f(){} };
class Derived : public Base {};

int main () {
  try {
    Base* a = new Base;
    Base* b = new Derived;
    cout << "a is: " << typeid(a).name() << '\n';
    cout << "b is: " << typeid(b).name() << '\n';
    cout << "*a is: " << typeid(*a).name() << '\n';
    cout << "*b is: " << typeid(*b).name() << '\n';
  } catch (exception& e) { cout << "Exception: " << e.what() << '\n'; }
  return 0;
}
程序輸出:

a is: class Base *
b is: class Base *
*a is: class Base
*b is: class Derived

記住:type_info的name成員所返回的字符串是依賴於你的編譯器和庫實現的。並不必定是典型的類名,好比編譯器將會生成它特有的輸出(PS: gcc 就不會生成上面這麼好看的類型名)。

記住typeid用於指針考慮的類型是指針的類型(a, b的類型都是Base*)。然而,當typeid被用在對象上(如*a 和*b),typeid產生它們的動態類型(好比,它們終於派生的完整對象)。

假設typeid評估的類型是一個指針解引用計算獲得的值,且這個指針指向一個空值,typeid將會拋出一個bad_typeid異常。(PS: 因而可知,使用dynamic_cast和typeid的代碼,需要考慮異常安全的問題)


後記

一些C++概念性的名詞記得不是很是清楚了,都給了原文,互相對比,讀者自行斟酌。另外,我調整了dynamic_cast在文中的排序,緣由有二。一是static_cast和interpret_cast和C-like轉型的做用相似,應與前文靠近。二是dynamic_cast和typeid都和RTTI有關,應與後文靠近。

若認爲我翻譯的有問題可以留言或郵件交流。

相關文章
相關標籤/搜索