类型转换

ZaynPei Lv6

C++ 的类型转换分为两大类:隐式类型转换和显式类型转换

隐式类型转换 (Implicit Type Conversion)

隐式类型转换是由编译器自动进行的,无需程序员显式指定。这通常发生在以下几种情况:

  1. 算术转换 (Arithmetic Conversion):在混合类型的算术表达式中,较小的类型会被提升(promote)为较大的类型以保证精度。

    1
    2
    3
    4
    // 例如:int 和 double 运算时,int 会被自动转换为 double。
    int i = 5;
    double d = 3.14;
    double result = i + d; // i 被隐式转换为 double 5.0,然后与 3.14 相加

  2. 赋值转换 (Assignment Conversion):将一个类型的值赋给另一个类型的变量时。

    1
    2
    3
    4
    // 在此处,double 类型转换为 int 类型,会丢失精度,这是一种潜在的数据丢失风险。
    int i;
    double d = 9.8;
    i = d; // d 的值被截断,小数部分丢失,i 的值为 9

  3. 指针转换 (Pointer Conversion):派生类的指针或引用可以被隐式转换为基类的指针或引用。这是支持多态性的基础。

    1
    2
    3
    4
    5
    6
    class Base {};
    class Derived : public Base {};

    Derived* p_derived = new Derived();
    Base* p_base = p_derived; // 派生类指针隐式转换为基类指针
    Base* p_base2 = new Derived(); // 更多时候,直接 new 一个派生类对象赋给基类指针

显式类型转换 (Explicit Type Conversion)

显式类型转换,也称为强制类型转换(Casting),是程序员明确要求的转换。C++ 从 C 语言继承了强制转换的语法,并增加了四个功能更明确、更安全的转换操作符。

C 风格强制转换 (C-Style Cast)

这是从 C 语言继承来的语法,形式为: (new_type)expression

1
2
3
int a = 10;
int b = 4;
double result = (double)a / b; // 将 a 转换为 double,结果为 2.5
C 风格转换的缺点在于它过于强大和不安全,可能会执行多种不同类型的转换(如 static_cast、const_cast、reinterpret_cast),这使得代码难以理解和维护。

  • 过于粗暴:C 风格的转换符像一把“万能钥匙”,它会依次尝试 static_cast、const_cast、reinterpret_cast,直到找到一个可以工作的。这使得它的行为难以预测,可能会执行一些非常危险的转换。

  • 意图不明:当你在代码中看到一个 C 风格转换时,你很难一眼看出程序员的真实意图。他是想进行一个安全的数值转换,还是想进行一个危险的指针类型重解释?

  • 难以搜索:在大型项目中,想要找出所有的类型转换是非常困难的,因为 () 符号在代码中太常见了。而 C++ 的 *_cast 关键字则非常容易搜索。

C++ 风格转换操作符

C++ 引入了四个新的转换操作符,它们的功能更具体,意图更明确,也更安全。

static_cast(expression): 用于“良性”或“合理”的转换,其正确性在编译时检查就可以确定。它是最常用的转换操作符。

  • 相关类型之间的转换:如数值类型之间的转换(int 到 double)、void* 指针与其他类型指针之间的转换。

  • 类层次结构中的转换:

    • 上行转换(安全):将派生类的指针或引用转换为基类的指针或引用(与隐式转换相同)。
    • 下行转换(不安全):将基类的指针或引用转换为派生类的指针或引用, 由于这属于多态, 而static_cast不进行运行时检查, 因此这需要程序员自己保证转换是安全的,即基类指针确实指向一个派生类对象。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 1. 基本类型转换
      double d = 3.14;
      int i = static_cast<int>(d); // i 的值为 3

      // 2. 类层次结构转换 (下行转换)
      class Base { public: virtual ~Base() {} };
      class Derived : public Base {};

      Base* p_base = new Derived();
      // 程序员确信 p_base 指向的是一个 Derived 对象,可以进行下行转换
      Derived* p_derived = static_cast<Derived*>(p_base);

这里的 static_cast 进行了下行转换。但它不会在运行时进行检查。如果 p_base 实际上指向的不是 Derived 对象,这个操作将导致未定义行为(运行时)。例如:

1
2
Base* p_base = new Base(); // 实际上指向 Base 对象
Derived* p_derived = static_cast<Derived*>(p_base); // 未定义行为!但不会在编译时报错

dynamic_cast(expression): dynamic_cast专门用于处理多态类型,在运行时进行类型检查,以确保下行转换的安全性

主要用于安全的类层次结构下行转换:在多态(基类必须有虚函数)的类继承体系中,将基类指针/引用安全地转换为派生类指针/引用。

  • 运行时检查:它会检查转换是否有效。
  • 对指针操作:如果转换成功,返回指向派生类对象的指针;如果转换失败(即基类指针并非指向目标派生类对象),返回 nullptr。
  • 对引用操作:如果转换成功,返回派生类的引用;如果转换失败,会抛出 std::bad_cast 异常。

前提:必须用于至少包含一个虚函数(virtual function)的基类,因为它依赖于运行时类型信息(RTTI)。

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
28
29
#include <iostream>

class Base { public: virtual void who() { std::cout << "I am Base\n"; } };
class Derived : public Base { public: void who() override { std::cout << "I am Derived\n"; } };
class Other : public Base { public: void who() override { std::cout << "I am Other\n"; } };

void check_type(Base* p) {
// 尝试将 Base* 转换为 Derived*, 这里 dynamic_cast 是创建一个新的指针变量,而不是改变原来指针的类型, 因此 p 本身的类型仍然是 Base*
Derived* p_derived = dynamic_cast<Derived*>(p);

if (p_derived != nullptr) {
std::cout << "Cast to Derived successful.\n";
p_derived->who();
} else {
std::cout << "Cast to Derived failed (p is not pointing to a Derived object).\n";
}
}

int main() {
Base* b1 = new Derived(); // 上行转换,隐式完成
Base* b2 = new Other();

check_type(b1); // 输出: Cast to Derived successful. I am Derived
check_type(b2); // 输出: Cast to Derived failed (p is not pointing to a Derived object).

delete b1;
delete b2;
return 0;
}

const_cast(expression): 是唯一能修改 constvolatile 属性的转换操作符, 用于去除对象的常量性。它只能添加或移除 const/volatile 属性,不能改变对象的实际类型

  • 移除 const 属性:将一个 const 指针/引用转换为非 const 指针/引用。
  • 增加 const 属性:将一个非 const 指针/引用转换为 const 指针/引用(这通常是安全的,可以隐式完成,但也可以显式使用 const_cast)。

使用 const_cast 移除 const 属性后,如果试图修改一个本身被定义为 const 的对象,其行为是未定义的。它主要用于这样的场景:你有一个 const 指针/引用,但你知道它指向的对象本身不是 const 的,你需要调用一个不接受 const 参数的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void legacy_function(int* p) { // 一个老旧的、不接受 const 指针的函数
*p = 20;
}

int main() {
const int val = 10;
// legacy_function(&val); // 错误:无法将 const int* 转换为 int*

// 警告:对原始的 const 变量进行修改是未定义行为!
// const_cast<int*>(&val) 虽然可以通过编译,但运行时行为未定义!val完全可能存储在只读内存中。
// legacy_function(const_cast<int*>(&val));

int non_const_val = 15;
const int* p_const = &non_const_val;
// 这种情况是安全的,因为 p_const 指向的对象 non_const_val 本身不是 const
legacy_function(const_cast<int*>(p_const));
// 现在 non_const_val 的值是 20
}

reinterpret_cast(expression): 用于低级别的重新解释类型,仅仅是重新解释给定的位模式,非常不安全。它通常用于与硬件打交道或进行底层编程。

  • 不同类型的指针之间转换:如将 int* 转换为 char*。
  • 指针与整数之间的转换:将指针转换为一个足以容纳它的整数类型,反之亦然。

这是最危险的转换操作符。它不进行任何类型检查,只是简单地告诉编译器“把这些二进制位当成另一种类型来看待”。它几乎总是不可移植的,应仅在绝对必要时(如与硬件交互的底层代码)使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int main() {
int value = 0x41424344; // 在小端系统中代表 'D', 'C', 'B', 'A'
int* p_int = &value;

// 将 int* 重新解释为 char*
char* p_char = reinterpret_cast<char*>(p_int);

// 逐字节打印整数的内存表示
for (int i = 0; i < sizeof(int); ++i) {
std::cout << *(p_char + i);
}
std::cout << std::endl; // 在小端系统上输出:DCBA
return 0;
}

最佳实践:

  • 优先使用 C++ 风格转换:它们更安全、意图更明确、更易于搜索和维护。

  • 尽量避免转换:如果你的代码中充斥着大量的类型转换,这通常是设计不良的信号。考虑使用多态、模板或更好的设计模式来避免转换。

  • 选择最合适的转换符:

    • 当你需要在相关类型之间进行转换时,static_cast 是首选。
    • 当你需要在多态类体系中安全地进行下行转换时,使用 dynamic_cast。
    • 当你需要处理 constvolatile 属性时(通常是为了兼容旧代码),只能使用 const_cast,并要格外小心。
    • 只有在进行非常底层的、与硬件相关的、并且你完全清楚自己在做什么时,才使用 reinterpret_cast。