第 19 節

C++类型转换

0瀏覽次數0訪問次數--跳出率--平均停留

本节解决什么问题

C++ 中经常会遇到“把一种类型当成另一种类型使用”的场景,例如:

  • double 转成 int
  • enum class 转成整数。
  • 把基类指针安全地转成派生类指针。
  • 临时去掉 const 限定,适配旧接口。
  • 在底层代码里把指针、整数、字节视图互相转换。

C 风格强转写法 (目标类型)表达式 能做很多事,但问题也在这里:它太“万能”,读代码的人很难一眼看出这次转换到底是普通数值转换、继承层级转换、去掉 const,还是危险的底层重解释。

C++ 提供了四个更明确的类型转换运算符,让转换意图写在代码里。

隐式转换和显式转换

类型转换可以分为隐式转换和显式转换。

隐式转换

隐式转换是由编译器自动完成的转换,代码中不需要写出转换操作。例如:

int count = 10;
double result = count;

这里 countint,而 resultdouble。编译器会自动把整数 10 转换成浮点数 10.0,这就是隐式转换。

隐式转换写起来方便,但有些转换可能造成数据丢失:

double price = 19.8;
int whole_price = price;

这段代码通常可以通过编译,但 whole_price 得到的是 19,小数部分会被丢弃。因为转换没有明确写出来,阅读代码时也容易忽略它。

显式转换

显式转换是程序员在代码中明确要求进行的转换。例如:

double price = 19.8;
int whole_price = static_cast<int>(price);

这里的 static_cast<int> 明确表示:程序员知道 price 会被转换成 int,也接受小数部分被丢弃。

两者的区别可以简单理解为:

  • 隐式转换:编译器根据上下文自动转换。
  • 显式转换:程序员在代码中明确写出要转换成什么类型。

隐式转换并不一定不安全,显式转换也不代表一定安全。显式写出转换的主要价值,是让转换意图更清楚,并让编译器按照指定的转换方式进行检查。

下面介绍的 static_castdynamic_castconst_castreinterpret_cast,都是 C++ 提供的显式类型转换。

这个特性是什么

C++ 的四种显式类型转换是:

转换方式主要用途安全程度
static_cast<T>(expr)常规类型转换,如数值、枚举、父子类上行转换常用,编译期检查
dynamic_cast<T>(expr)多态类型的安全向下转换运行期检查,失败返回 nullptr 或抛异常
const_cast<T>(expr)添加或移除 const / volatile 限定要非常克制
reinterpret_cast<T>(expr)底层二进制重解释,如指针和整数互转最危险,少用

原则:能不用强转就不用;必须强转时,优先选择语义最窄、最能说明意图的转换。

C++ 标准版本

C++98 就已经提供这四种转换写法,实际工程中依然推荐使用它们替代 C 风格强转。

需要的头文件

类型转换本身不需要额外头文件。示例代码使用到的库需要对应头文件,例如:

#include <cstdint>
#include <iostream>

运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。

基本语法

目标类型 value = static_cast<目标类型>(表达式);
目标类型 value = dynamic_cast<目标类型>(表达式);
目标类型 value = const_cast<目标类型>(表达式);
目标类型 value = reinterpret_cast<目标类型>(表达式);

运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。

示例代码

示例 1:static_cast 处理常规转换

static_cast 适合明确、常规、编译期能检查的转换。比如数值转换、enum class 转整数、父类指针指向子类对象等。

#include <iostream>

enum class Status
{
    ok = 200,
    not_found = 404
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    double score = 89.7;
    int integer_score = static_cast<int>(score);

    Status status = Status::not_found;
    int status_code = static_cast<int>(status);

    std::cout << "integer_score = " << integer_score << "\n";
    std::cout << "status_code = " << status_code << "\n";

    // 返回 0 表示程序正常结束。
    return 0;
}

运行结果

integer_score = 89
status_code = 404

示例 2:dynamic_cast 安全判断真实对象类型

dynamic_cast 常用于多态基类。向下转换时,它会在运行期检查对象真实类型。

#include <iostream>

struct Animal
{
    virtual ~Animal() = default;
};

struct Cat : Animal
{
    void meow() const
    {
        std::cout << "cat: meow\n";
    }
};

struct Dog : Animal
{
};

void try_meow(Animal* animal)
{
    Cat* cat = dynamic_cast<Cat*>(animal);

    if (cat != nullptr)
    {
        cat->meow();
    }
    else
    {
        std::cout << "not a cat\n";
    }
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    Cat cat;
    Dog dog;

    try_meow(&cat);
    try_meow(&dog);

    // 返回 0 表示程序正常结束。
    return 0;
}

运行结果

cat: meow
not a cat

注意:dynamic_cast 用在指针上,转换失败会得到 nullptr;用在引用上,转换失败会抛出 std::bad_cast。它要求基类至少有一个虚函数,通常是虚析构函数。

示例 3:const_cast 只改变 const 限定

const_cast 只能添加或移除 const / volatile 限定,不能把 int 转成 double,也不能把两个不相关的类型互相转换。

#include <iostream>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    int value = 10;
    const int& readonly_ref = value;

    int& writable_ref = const_cast<int&>(readonly_ref);
    writable_ref = 20;

    std::cout << "value = " << value << "\n";

    // 返回 0 表示程序正常结束。
    return 0;
}

运行结果

value = 20

这个例子里,value 本身是一个普通的 int 变量,并不是 const int

int value = 10;
const int& readonly_ref = value;

这两行代码的意思是:readonly_refvalue 的一个“只读引用”。它并没有创建一个新的变量,而是给 value 起了一个只能读取、不能修改的别名。

也就是说:

readonly_ref

本质上还是指向原来的 value

接下来:

int& writable_ref = const_cast<int&>(readonly_ref);

这行代码使用 const_cast 去掉了 readonly_ref 上的 const 限定,让它重新变成一个可以修改的引用。由于它引用的还是原来的 value,所以:

writable_ref = 20;

本质上就是在修改:

value = 20;

因此最后输出的是:

value = 20

需要注意的是,这个例子能够修改成功,是因为原始对象 value 本身不是 const,只是我们临时用 const int& 这种只读方式去引用它。

如果原始对象本身就是 const,再用 const_cast 强行修改就是未定义行为,例如:

const int value = 10;
const int& readonly_ref = value;

int& writable_ref = const_cast<int&>(readonly_ref);
writable_ref = 20; // 未定义行为

所以可以简单记住:

const_cast 只是把“只读视角”改回“可写视角”,它不会复制出新变量,也不会真的把一个天生的常量安全地变成普通变量。

示例 4:reinterpret_cast 做底层重解释

reinterpret_cast 表示“把这段二进制数据按另一种类型解释”。它绕过了很多类型系统保护,通常只应该出现在非常底层的代码里。

比如把一个指针转换成整数形式的地址,再把这个整数地址转换回指针。

reinterpret_cast 可以用来做一些比较“底层”的类型转换,比如把一个指针转换成整数形式的地址,再把这个整数地址转换回指针。

需要注意的是,这里转换的是地址,不是变量里面保存的值。

#include <cstdint>
#include <iomanip>
#include <iostream>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    int value = 42;
    int* p = &value;

    std::uintptr_t raw = reinterpret_cast<std::uintptr_t>(p);
    int* again = reinterpret_cast<int*>(raw);

    std::cout << "value = " << value << "\n";
    std::cout << "p = " << p << "\n";

    std::cout << "raw = " << raw << "\n";
    std::cout << "raw(hex) = 0x" << std::hex << raw << std::dec << "\n";

    std::cout << "again = " << again << "\n";
    std::cout << "*again = " << *again << "\n";

    std::cout << std::boolalpha;
    std::cout << "same pointer = " << (p == again) << "\n";

    // 返回 0 表示程序正常结束。
    return 0;
}

运行结果示例

value = 42
p = 0x7ffd60a576d4
raw = 140726224910036
raw(hex) = 0x7ffd60a576d4
again = 0x7ffd60a576d4
*again = 42
same pointer = true

其中,具体的地址值每次运行都可能不同,所以你的运行结果不一定和上面完全一样。

这段代码里:

int value = 42;
int* p = &value;

value 是一个普通的 int 变量,里面保存的值是 42

p 是一个 int* 指针,里面保存的是 value 的地址,而不是 value 的值。

也就是说,p 里面存的不是:

42

而是类似这样的地址:

0x7ffd60a576d4

接着:

std::uintptr_t raw = reinterpret_cast<std::uintptr_t>(p);

这行代码把指针 p 转换成了一个整数。

std::uintptr_t 是一种无符号整数类型,它的作用是尽可能安全地保存一个指针地址。

所以如果:

p = 0x7ffd60a576d4

那么 raw 保存的就是这个地址对应的整数形式。

例如:

raw = 140726224910036

这只是同一个地址的十进制写法。

下面这行代码:

std::cout << "raw(hex) = 0x" << std::hex << raw << std::dec << "\n";

是把 raw 按十六进制打印出来。这样就可以看到,raw 的十六进制形式和指针 p 打印出来的地址基本是一样的。

然后:

int* again = reinterpret_cast<int*>(raw);

这行代码又把整数形式的地址 raw 转回了 int* 指针。

也就是说:

p     指向 value
again 也指向 value

所以:

std::cout << "*again = " << *again << "\n";

这里的 *again 就是在访问 again 指向的变量。

因为 again 重新指向了原来的 value,所以:

*again

等价于:

value

因此输出结果是:

*again = 42

最后:

std::cout << "same pointer = " << (p == again) << "\n";

这行代码比较的是 pagain 这两个指针保存的地址是否相同。

由于它们都指向 value,所以结果是:

same pointer = true

可以把整个过程理解成:

value 是一个变量,值是 42

p 保存 value 的地址

raw 把这个地址用整数形式保存下来

again 又把这个整数地址转换回指针

所以 again 重新指向 value

所以这段代码的重点不是把 42 转成整数,而是把 value地址转成整数。

简单记忆:

int* p = &value;

表示:

拿到 value 的地址
std::uintptr_t raw = reinterpret_cast<std::uintptr_t>(p);

表示:

把地址当成整数保存
int* again = reinterpret_cast<int*>(raw);

表示:

把整数地址再变回指针
*again

表示:

通过这个地址找到原来的 value

一句话总结:

reinterpret_cast 在这个例子里转换的是“地址的表示形式”,不是变量里面的值。value 的值是 42,而 raw 保存的是 value 的地址。

四种转换怎么选

需求推荐写法说明
doubleintstatic_cast<int>(x)明确可能丢失小数
enum class 转整数static_cast<int>(e)枚举类不会隐式转整数
基类指针转派生类指针,并且不确定真实类型dynamic_cast<Derived*>(p)转换失败可判断 nullptr
去掉 const 适配旧接口const_cast<T*>(p)前提是旧接口不会修改真正的 const 对象
指针和整数互转、底层字节解释reinterpret_cast<T>(x)非常底层,优先避免

类型转换 对比 C风格强转 会增加运行开销吗

static_cast<T>(expr) 中虽然使用了尖括号,看起来有些像模板,但它不是模板,而是 C++ 语言内置的类型转换运算符。这里的 T 表示要转换成的目标类型。

与功能相同的 C 风格强转相比,C++ 的显式类型转换通常不会产生额外的运行开销:

转换方式通常的运行开销
static_cast不会因为使用这种写法而增加额外开销
dynamic_cast需要在运行时检查对象的真实类型,可能产生时间和程序体积开销
const_cast通常只改变编译器进行类型检查的方式,没有运行时操作
reinterpret_cast通常只改变编译器解释数据的方式,没有额外转换步骤

例如,下面两种写法表达的是同一种数值转换:

double value = 3.14;

int a = (int)value;
int b = static_cast<int>(value);

编译器通常会为它们生成相同或等价的机器指令。static_cast 的优势不是运行得更快,而是转换意图更加明确,并且能够得到更严格的编译期检查。

不过,需要区分“转换写法的开销”和“转换操作本身的开销”。例如,把浮点数转换成整数可能需要执行实际的处理器指令:

double value = 3.14;
int result = static_cast<int>(value);

这里可能产生的运行开销来自“把 double 转换成 int”这个操作,而不是来自 static_cast 语法。即使改成隐式转换或 C 风格强转,这个转换操作仍然存在。

因此可以简单记住:选择 C++ 的显式类型转换,主要是为了让类型检查更严格、代码意图更清楚;除了需要运行时检查的 dynamic_cast,它通常不会因为写法不同而比 C 风格强转更慢。

C风格强转为什么不推荐

C 风格强转写起来短:

int n = (int)3.14;

运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。

更推荐写成:

int n = static_cast<int>(3.14);

运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。

C 风格强转可能在背后组合多种转换能力,既可能像 static_cast,也可能像 const_cast,甚至接近 reinterpret_cast。代码越底层、类型越复杂,这种“不说明白”的风险就越大。

常见错误

用 static_cast 做不安全向下转换

Animal* animal = new Dog;
Cat* cat = static_cast<Cat*>(animal);  // 危险:编译可能通过,但真实对象不是 Cat

运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。

如果不确定真实类型,应该使用 dynamic_cast

修改真正的 const 对象

const int value = 10;
int& ref = const_cast<int&>(value);
ref = 20;  // 未定义行为

运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。

滥用 reinterpret_cast

double d = 3.14;
int* p = reinterpret_cast<int*>(&d);  // 危险:把 double 对象当 int 对象访问

运行结果: 此代码块不含 main 函数,是语法、接口或分文件示例,不能独立运行,因此没有终端输出。

使用建议

  1. 普通转换优先用 static_cast
  2. 多态向下转换优先用 dynamic_cast,并检查失败情况。
  3. const_cast 只用于适配旧接口,不要用它修改真正的 const 对象。
  4. reinterpret_cast 只放在非常底层、边界清晰的代码里,并集中封装。
  5. 不要为了少打几个字使用 C 风格强转。

小结

  • static_cast:常规转换,最常用。
  • dynamic_cast:多态类型安全向下转换。
  • const_cast:只改变 const / volatile 限定。
  • reinterpret_cast:底层重解释,风险最高。

一句话记忆:能不用就不用;必须用时,把转换意图写清楚。

音乐页