型別轉換

隱式轉換

當一個值被複制到相容型別時,會自動執行隱式轉換。例如

1
2
3
short a=2000;
int b;
b=a;

這裡,`a` 的值從 `short` 提升為 `int`,無需任何顯式運算子。這被稱為*標準轉換*。標準轉換影響基本資料型別,並允許數值型別之間的轉換(`short` 到 `int`、`int` 到 `float`、`double` 到 `int`...)、與 `bool` 之間的轉換,以及一些指標轉換。

從某種較小的整數型別轉換為 `int`,或從 `float` 轉換為 `double` 被稱為*提升*,並且保證在目標型別中產生完全相同的值。其他算術型別之間的轉換可能無法始終精確表示相同的值
  • 如果一個負整數值被轉換為無符號型別,則結果值對應於其 2 的補碼按位表示(即,`-1` 變為該型別可表示的最大值,`-2` 變為次大值,依此類推)。
  • 與 `bool` 之間的轉換將 `false` 視為*零*(對於數值型別)和*空指標*(對於指標型別);`true` 等同於所有其他值,並轉換為等效於 `1` 的值。
  • 如果轉換是從浮點型別到整數型別,則值將被截斷(刪除小數部分)。如果結果超出了該型別可表示值的範圍,則轉換會導致*未定義行為*。
  • 否則,如果轉換髮生在相同種類的數值型別之間(整數到整數或浮點到浮點),則轉換是有效的,但值是*特定於實現的*(並且可能不可移植)。

這些轉換中的一些可能涉及精度損失,編譯器會用警告訊號。可以透過顯式轉換來避免此警告。

對於非基本型別,陣列和函式隱式轉換為指標,而指標通常允許以下轉換
  • *空指標*可以轉換為任何型別的指標
  • 指向任何型別的指標都可以轉換為 `void` 指標。
  • 指標*向上轉型*:指向派生類的指標可以轉換為*可訪問*且*無歧義*的基類指標,而不會修改其 `const` 或 `volatile` 限定。

類的隱式轉換

在類的世界中,隱式轉換可以透過三種成員函式來控制
  • 單引數建構函式:允許從特定型別進行隱式轉換以初始化物件。
  • 賦值運算子:允許在賦值時從特定型別進行隱式轉換。
  • 型別轉換運算子:允許隱式轉換為特定型別。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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;
}
 

型別轉換運算子使用特定語法:它使用 `operator` 關鍵字,後跟目標型別和一對空括號。請注意,返回型別是目標型別,因此不在 `operator` 關鍵字之前指定。

關鍵字 explicit

在函式呼叫時,C++ 允許對每個引數執行一次隱式轉換。這對於類來說可能有點問題,因為它並不總是如預期的那樣。例如,如果我們向最後一個示例新增以下函式

1
void fn (B arg) {}

此函式接受一個 `B` 型別的引數,但它也可以用 `A` 型別的物件作為引數來呼叫

1
fn (foo);

這可能符合預期,也可能不符合。但是,無論如何,可以透過用 `explicit` 關鍵字標記受影響的建構函式來防止這種情況。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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;
}

此外,用 `explicit` 標記的建構函式不能使用類似賦值的語法呼叫;在上面的示例中,`bar` 不能透過以下方式構造:

1
B bar = foo;

型別轉換成員函式(前面部分所述)也可以指定為 `explicit`。這以與 `explicit` 指定的建構函式對目標型別一樣的方式防止隱式轉換。

型別轉換

C++ 是一種強型別語言。許多轉換,尤其是那些涉及值不同解釋的轉換,需要顯式轉換,在 C++ 中稱為*型別轉換*。有兩種主要的通用型別轉換語法:*函式式*和*類 C 風格*。

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

這些通用型別轉換形式的功能足以滿足基本資料型別的絕大多數需求。然而,這些運算子可以無差別地應用於類和類指標,這可能導致程式碼在語法上正確但會導致執行時錯誤。例如,以下程式碼可以編譯而沒有錯誤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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` 的指標,但隨後透過顯式型別轉換將一個不相關型別物件的引用賦值給它。

1
padd = (Addition*) &d;

無限制的顯式型別轉換允許將任何指標轉換為任何其他指標型別,而與它們指向的型別無關。後續對成員 `result` 的呼叫將導致執行時錯誤或某些其他意外結果。

為了控制這些類之間的轉換,我們有四個特定的轉換運算子:`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)

但每個都有其獨特的特性

dynamic_cast

`dynamic_cast` 只能用於類指標和引用(或 `void*`)。其目的是確保型別轉換的結果指向目標指標型別的有效完整物件。

這自然包括*指標向上轉型*(從派生類指標轉換為基類指標),就像*隱式轉換*允許的那樣。

但是 `dynamic_cast` 也可以*向下轉型*(從基類指標轉換為派生類指標)多型類(具有虛擬函式的類),前提是——並且僅當——指向的物件是目標型別的有效完整物件。例如

// 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` 需要*執行時型別資訊 (RTTI)* 來跟蹤動態型別。某些編譯器支援此功能作為一個預設停用的選項。需要啟用此功能才能使使用 `dynamic_cast` 的執行時型別檢查與這些型別正確工作。

上面的程式碼嘗試將兩個 `Base*` 型別的指標物件(`pba` 和 `pbb`)動態轉換為 `Derived*` 型別的指標物件,但只有第一個成功。請注意它們的相應初始化

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

儘管兩者都是 `Base*` 型別的指標,但 `pba` 實際上指向一個 `Derived` 型別的物件,而 `pbb` 指向一個 `Base` 型別的物件。因此,當使用 `dynamic_cast` 執行各自的型別轉換時,`pba` 指向一個完整的 `Derived` 類物件,而 `pbb` 指向一個 `Base` 類物件,這是一個不完整的 `Derived` 類物件。

當 `dynamic_cast` 由於指向的物件不是所需類的完整物件而無法轉換指標時(如上例中的第二個轉換),它將返回一個*空指標*來指示失敗。如果 `dynamic_cast` 用於轉換為引用型別且轉換不可行,則會丟擲 `bad_cast` 型別的異常。

`dynamic_cast` 還可以執行指標允許的其他隱式轉換:在指標型別之間(即使是無關的類之間)轉換空指標,以及將任何型別的指標轉換為 `void*` 指標。

static_cast

`static_cast` 可以執行相關類指標之間的轉換,不僅是*向上轉型*(從派生類指標到基類指標),還可以進行*向下轉型*(從基類指標到派生類指標)。執行時不會執行任何檢查以保證被轉換的物件實際上是目標型別的完整物件。因此,程式設計師有責任確保轉換是安全的。另一方面,它不會產生 `dynamic_cast` 的型別安全檢查的開銷。

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

這將是有效的程式碼,儘管 `b` 將指向類的不完整物件,並且在解引用時可能導致執行時錯誤。

因此,`static_cast` 能夠對類指標執行不僅是隱式允許的轉換,還有它們的逆向轉換。

`static_cast` 還能執行所有隱式允許的轉換(不僅是類指標的轉換),並且還能執行這些轉換的逆向操作。它可以
  • 從 `void*` 轉換為任何指標型別。在這種情況下,它保證如果 `void*` 值是透過從相同指標型別轉換獲得的,則結果指標值是相同的。
  • 將整數、浮點值和列舉型別轉換為列舉型別。

此外,`static_cast` 還可以執行以下操作
  • 顯式呼叫單引數建構函式或轉換運算子。
  • 轉換為*右值引用*。
  • 將 `enum class` 值轉換為整數或浮點值。
  • 將任何型別轉換為 `void`,計算並丟棄該值。

reinterpret_cast

`reinterpret_cast` 將任何指標型別轉換為任何其他指標型別,即使是不相關的類。操作結果是從一個指標到另一個指標的簡單二進位制複製。允許所有指標轉換:既不檢查指向的內容,也不檢查指標型別本身。

它還可以將指標轉換為整數型別或從整數型別轉換。此整數值表示指標的格式是平臺特定的。唯一的保證是,一個足夠大的整數型別(如 `intptr_t`)的指標,可以被轉換回一個有效的指標。

`reinterpret_cast` 可以執行但 `static_cast` 不能執行的轉換是基於重新解釋型別二進位制表示的低階操作,在大多數情況下會產生系統特定且因此不可移植的程式碼。例如

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

此程式碼可以編譯,儘管意義不大,因為現在 `b` 指向一個完全不相關且可能不相容的類的物件。解引用 `b` 是不安全的。

const_cast

此類轉換操作指向物件的 `const` 性質,可以設定或移除。例如,為了將一個 const 指標傳遞給一個需要非 const 引數的函式

// 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;
}
sample text

上面的示例保證可以工作,因為函式 `print` 不會寫入指向的物件。但請注意,移除指向物件的 `const` 性質以實際寫入它會導致*未定義行為*。

typeid

`typeid` 允許檢查表示式的型別

typeid (expression)

此運算子返回一個指向標準標頭檔案 `<typeinfo>` 中定義的 `type_info` 型別的常量物件的引用。 `typeid` 返回的值可以使用 `==` 和 `!=` 運算子與另一個 `typeid` 返回的值進行比較,或者透過使用其 `name()` 成員來獲取表示資料型別或類名稱的以 null 結尾的字元序列。

// 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 來跟蹤動態物件的型別。當 `typeid` 應用於型別為多型類的表示式時,結果是最派生完整物件的型別。

// 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` 返回的字串取決於您的編譯器和庫的具體實現。它不一定是具有典型型別名稱的簡單字串,就像用於生成此輸出的編譯器一樣。

注意 `typeid` 考慮的指標型別是指標型別本身(`a` 和 `b` 都是 `class Base *` 型別)。但是,當 `typeid` 應用於物件(如 `*a` 和 `*b`)時,`typeid` 會產生它們的動態型別(即它們最派生完整物件的型別)。

如果 `typeid` 求值的型別是指標,前面帶有解引用運算子 (`*`),並且該指標的值為空,則 `typeid` 會丟擲 `bad_typeid` 異常。
Index
目錄