需要预先了解:


1. 隐式类型转换

隐式类型转换(implicit conversion)由编译器自动完成,不需要程序员写出任何转换语法。它发生在赋值、传参、表达式求值等场景中,正确使用时十分自然,但某些情况下会悄悄引入错误。


1.1 数值转换与整型提升

小整型参与算术运算时,会先提升为 int,这叫整型提升(integral promotion):

char c = 'A';
int n = c;          // char 提升为 int,n = 65

short s = 100;
int sum = s + 1;    // s 先提升为 int 再参与加法

从窄到宽的数值转换是安全的:int 转 long、float 转 double 都不丢失信息。但从宽到窄的转换会截断或丢失精度,比如 double 转 int 直接丢掉小数部分:

double d = 3.99;
int x = d;          // x = 3,小数部分被丢弃

浮点转整数的截断方向是向零取整(truncation),不是四舍五入,-3.99 转 int 得到 -3。


1.2 有符号与无符号的陷阱

有符号和无符号类型混合运算时,有符号值会被隐式转换为无符号类型,这是隐式转换最经典的坑:

#include <iostream>

int main()
{
    int a = -1;
    unsigned int b = 1;

    std::cout << (a < b) << std::endl;   // 输出 0(false)
}

a 被转换为 unsigned int 后变成 4294967295,自然不小于 1。类似地,-1 > 0U 在无符号视角下也是成立的。规则:转换时值按无符号类型的模(mod,即 2 的 N 次方,N 为位宽)取余,负数会"回绕"成很大的正数。


1.3 指针与类类型的隐式转换

指针相关的隐式转换:

  • 数组名退化为指针:数组在表达式中自动退化为指向首元素的指针
  • 向上转型(upcast):派生类指针/引用隐式转换为基类指针/引用,多态的基础
  • nullptr 可以隐式转换为任意指针类型
int arr[3] = {1, 2, 3};
int* p = arr;              // 数组名退化为指针

class Base {};
class Derived : public Base {};
Derived d;
Base* pb = &d;             // 向上转型,隐式完成

类类型的隐式转换有两个来源:

  1. 单参数构造函数:定义了从别的类型"构造"本类型的路径
  2. 转换运算符:定义了本类型"变回"别的类型的路径
class Number
{
public:
    Number(int v) : val(v) {}
    operator int() const { return val; }
private:
    int val;
};

Number num = 42;     // int -> Number,调用构造函数
int back = num;      // Number -> int,调用转换运算符

这种隐式转换在便利的同时也容易误触发,比如把 Number 意外传给只接受 int 的函数。如果不希望某个构造函数参与隐式转换,可以加 explicit 关键字禁止它。


2. C 风格类型转换

C 风格类型转换(C-style cast)写法是 (目标类型)表达式,语法最简单,但语义上它是一把"万能钥匙"——等价于依次尝试 const_cast、static_cast、static_cast + const_cast、reinterpret_cast、reinterpret_cast + const_cast,哪个能编译通过就用哪个:

double d = 3.14;
int x = (int)d;               // 数值截断(static_cast 的活)

const char* str = "hello";
char* p = (char*)str;         // 去掉 const(const_cast 的活)

int* ip = (int*)0x1234;       // 位级重解释(reinterpret_cast 的活)

从三个角度分析它的问题:

  • 可读性:(int) 看不出转换意图,是数值转换、去 const 还是位重解释?代码搜索时搜 static_cast 能精确定位,搜 (int) 会误伤一片
  • 安全性:编译器完全不检查转换的合理性,允许任意组合,容易写出危险代码而不自知
  • 性能:本身零运行时开销,但因为它混用了几种语义,一旦需要排查转换相关的 bug,定位成本很高

注:C++ 中应避免使用 C 风格转换,用四种具名 cast 明确表达转换意图。


3. static_cast

static_cast 用于"逻辑上相关"的类型之间,转换是否合法在编译期检查。典型用途:

  1. 数值类型互转、枚举与整数互转
  2. void* 与具体类型指针互转
  3. 类层次中的向上转型,以及不带检查的向下转型
#include <iostream>

int main()
{
    double d = 3.14;
    int x = static_cast<int>(d);        // 截断为 3

    int n = 42;
    void* vp = &n;                      // 任何指针都能隐式转 void*
    int* ip = static_cast<int*>(vp);    // void* 还原为具体指针

    std::cout << x << ' ' << *ip << std::endl;   // 输出 3 42
}

编译期检查意味着不相关的类型无法通过编译:

int n = 42;
double* dp = static_cast<double*>(&n);   // 错误!int* 与 double* 不相关

编译时报错:

code.cpp:5:32: error: invalid static_cast from type 'int*' to type 'double*'
    5 |     double* dp = static_cast<double*>(&n);

安全性分析:编译期能拦截大部分错误组合;性能上零运行时开销,不生成任何检查代码;可读性上意图明确(“相关类型之间的转换”)。

注意:static_cast 的向下转型(基类指针转派生类指针)不做运行时检查,如果实际对象不是目标派生类型,解引用就是未定义行为——不确定时应该用 dynamic_cast。


4. dynamic_cast

dynamic_cast 是运行时检查的向下转型,只能用于多态类型(含虚函数的类),依赖 RTTI(Run-Time Type Information,运行时类型信息):

#include <iostream>

class Base
{
public:
    virtual ~Base() {}      // 有虚函数才是多态类型
};

class Derived : public Base
{
public:
    void hello() const { std::cout << "Derived" << std::endl; }
};

int main()
{
    Base* b1 = new Derived();
    Base* b2 = new Base();

    Derived* d1 = dynamic_cast<Derived*>(b1);   // 成功:实际类型就是 Derived
    Derived* d2 = dynamic_cast<Derived*>(b2);   // 失败:返回 nullptr

    if (d1) d1->hello();    // 输出 Derived
    std::cout << (d2 == nullptr) << std::endl;  // 输出 1
}

行为规则:

  • 指针转换失败返回 nullptr,因此总是需要判空
  • 引用转换失败抛出 std::bad_cast 异常(引用没有"空"的概念)

从三个角度分析:

  • 安全性:最高——转换前在运行时核实对象的真实类型,失败有明确信号
  • 性能:有运行时开销,需要查 RTTI 信息做类型比较,比 static_cast 慢;虚函数表中的 typeinfo 指针参与判断
  • 可读性:语义清晰,代码本身就表达了"我不确定实际类型,需要检查"

注:dynamic_cast 只对有虚函数的类生效,因为类型信息存在虚函数表附近;对非多态类型做 dynamic_cast 编译直接报错。滥用 dynamic_cast 通常意味着设计上可以用虚函数替代。


5. const_cast

const_cast 只能添加或去除 const(以及 volatile)限定,不能改变类型本身:

#include <cstring>
#include <iostream>

void print_len(const char* s)
{
    std::cout << strlen(s) << std::endl;
}

int main()
{
    char buf[] = "hello";
    const char* cp = buf;                    // 对象本身不是 const,只是通过 const 指针看它
    print_len(cp);

    char* p = const_cast<char*>(cp);         // 去掉 const,安全:buf 本来就可写
    p[0] = 'H';
    print_len(buf);                          // 输出 5(内容变成 Hello)
}

安全性取决于对象本身是否真的是 const:

  • 对象原本不是 const(只是通过 const 指针/引用访问):去 const 后修改是安全的
  • 对象本身就是 const:const_cast 后修改它是未定义行为,可能直接崩溃或悄悄出错
const int a = 10;
int* p = const_cast<int*>(&a);
*p = 20;    // 未定义行为!a 是真正的 const 对象,编译器可能把 a 放在只读存储

const_cast 性能零开销,但它破坏类型系统的约束,属于"危险工具"。它最常见的正当用途是调用接口不友好(参数少了 const)的第三方库函数。


6. reinterpret_cast

reinterpret_cast 对内存中的位模式做位级重解释(bitwise reinterpretation),不进行任何检查,也不调整值——只是让编译器把同一块内存当成另一种类型来看:

#include <iostream>
#include <cstdint>

int main()
{
    int i = 0x3f800000;
    float f = reinterpret_cast<float&>(i);    // 把 int 的位模式当作 float 解读,f == 1.0f

    int n = 42;
    uintptr_t addr = reinterpret_cast<uintptr_t>(&n);   // 指针与整数互转

    std::cout << f << ' ' << std::hex << addr << std::endl;
}

0x3f800000 按 IEEE 754 解读正好是 1.0f——同样的 4 个字节,换个视角就是另一个数。

安全性分析:

  • 安全性最低:结果依赖平台对类型布局、大小端、对齐的假设,可移植性差;对齐不满足时解引用可能是未定义行为
  • 性能:零开销,不生成任何指令
  • 可读性:意图非常明确(“我要重解释这些位”),比 C 风格转换的隐式重解释更清晰

它只适合底层场景:硬件寄存器访问、网络协议解析、序列化、指针与整数互转等。


7. 四种 cast 的对比与选择

从安全性、性能、可读性三个维度整体对比:

转换方式 典型用途 安全性 运行时开销 可读性
隐式转换 数值提升、向上转型 中等(有符号/无符号陷阱) 零 高
C 风格转换 旧代码兼容 低 零 低(意图不明)
static_cast 相关类型转换、void* 还原 较高(编译期检查) 零 高
dynamic_cast 多态向下转型 高(运行时检查) 有(RTTI 查询) 高
const_cast 去 const 低(可能未定义行为) 零 高
reinterpret_cast 位级重解释、指针与整数互转 最低 零 高

选择原则:

  1. 能靠隐式转换解决的场景(数值提升、向上转型)直接用,不需要写 cast
  2. 需要显式转换时,默认选 static_cast——编译期检查 + 零开销 + 意图明确
  3. 多态向下转型且不确定实际类型,用 dynamic_cast,并接受它的运行时开销
  4. 不要写 C 风格转换,它把几种语义混在一起,无法表达意图
  5. const_cast 与 reinterpret_cast 是危险工具,只在底层或兼容场景使用,并且要用注释说明为什么必须这么做