类型转换

隐式转换

当一个值被复制到兼容类型时,会自动执行隐式转换。例如

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
目录