C++的类型转换

文章目录

C++的类型转换

C语言中的类型转换

C语言在赋值、函数传参、返回值接收等场景中,如果左右两侧类型不一致,就可能发生类型转换。C语言把类型转换分为两类:隐式类型转换 和显式类型转换 。

隐式转换由编译器在编译阶段自动完成,显式转换由程序员通过强制转换语法完成。 C风格转换的核心问题是"转换形式统一但语义不明确",容易把精度丢失、指针截断、去除 const 等风险混在一起,导致代码难以审查和排错。

c 复制代码
#include <iostream>
#include <cstdio>

int main()
{
    int i = 42;
    double d1 = i;  // 隐式类型转换:int -> double
    int x = 12.98; // 隐式类型转换:double -> int,小数部分被截断
    unsigned int u = 10;
    int y = -1;
    if (y < u)
    { // 有符号与无符号比较,y 可能被转换为很大的无符号数
        std::cout << "y < u 成立" << std::endl;
    }
    else
    {
        std::cout << "y < u 不成立" << std::endl;
    }
    std::cout << "d = " << d1 << ", x = " << x << std::endl;

    double d2 = 12.34;
    int a = (int)d2; // C风格显式转换:double -> int
    int value = 7;
    int *p = &value;
    int address = (int)p; // C风格显式转换:指针 -> int,可能发生截断
    std::cout << "a = " << a << ", address = " << address << std::endl;

    
    return 0;
}

我们看上述代码,我们能发现C语言的隐式类型转换问题其实挺大的。

那么为什么全部要求显示类型转换不就行了。

  • 确实全部使用显示类型转换可以解决上述隐式类型转换的问题,但是C语言的显示类型转换语法太简单了,(类型)变量; 就构成类型转换,这就会导致我们有时漏写或忘写的情况。通常这种忘写和漏写影响不是很大因为显示类型转换是要经过思考过后才决定的事,但是这还是会对后续代码进行维护留下了隐患。

为什么C++需要四种类型转换

C++ 在兼容 C风格转换的同时,引入了四种类型转换分别是:static_cast、reinterpret_cast、const_cast、dynamic_cast 。

那么为什么C++要将强制类型转换搞的这么复杂呢?

  • 因为C++ 需要四种命名转换,是因为类型转换的语义差异很大:数值转换 、位模式重解释 、去除 const 、继承体系中的安全向下转型,风险和检查规则完全不同。如果继续使用 C风格统一语法,代码审查者无法快速判断转换意图,编译器也难以针对不同转换做不同检查。让用户将意图写出来。

C风格转换的问题与C++设计动机

C风格转换主要有两个问题:第一,隐式转换可能悄悄丢精度或引发符号问题; 第二,显式转换把所有情况混在一起,代码不够清晰 。C++ 命名转换的设计动机是"按语义分类",例如 static_cast 表示相关类型间的静态转换,reinterpret_cast 表示低层位模式重解释,const_cast 表示只处理 const/volatile 属性,dynamic_cast 表示运行时检查的安全向下转型。这样写出来的代码更容易搜索、审查和定位错误。

cpp 复制代码
int main() 
{
    double d = 12.34;
    int a = (int)d;                         // C风格:看不出是普通数值转换
    int b = static_cast<int>(d);            // C++风格:明确是静态数值转换
    const int c = 10;
    int* p = (int*)&c;                      // C风格:看不出去了 const
    int* q = const_cast<int*>(&c);          // C++风格:明确只去 const
    std::cout << a << " " << b << " " << *p << " " << *q << std::endl;
    return 0;
}

注意:C++的强制类型转换并不是"万能药",它只是让意图更清晰;例如 reinterpret_cast 和 const_cast 仍然可能产生未定义行为。

C++强制类型转换

static_cast

static_cast 用于相关类型之间的静态转换,例如基本数值类型转换、void* 与具体指针类型转换、继承体系中基类与派生类指针/引用的相关转换等。它由编译器在编译期根据类型关系进行检查,不做运行时类型检查。它不能用于两个完全不相关的指针类型互转,也不能去除 const 属性 。它的优点是语义比 C风格转换清晰,但向下转型时如果实际对象不是目标派生类,仍然可能产生未定义行为。

cpp 复制代码
int main()
{
    double d = 12.34;
    int a = static_cast<int>(d); // 基本类型转换,小数部分截断
    std::cout << "a = " << a << std::endl;

    void *vp = &a;
    int *ip = static_cast<int *>(vp); // void* -> 具体指针类型
    std::cout << "*ip = " << *ip << std::endl;

    char *cp = new char('c');
    short *sp = static_cast<short*>(cp); // 报错:类型转换无效
    return 0;
}

static_cast 的问题就是不做运行时检查,尤其用于继承体系向下转型时,如果父类指针实际指向父类对象,强转为子类指针后使用可能崩溃或产生未定义行为。

reinterpret_cast

reinterpret_cast 用于低层次的位模式重新解释,通常把一种类型的位模式解释成另一种类型,例如指针与足够宽的整数互转、不同指针类型之间的重解释。它不进行类型安全检查,也不保证转换后的对象可合法访问。它的使用场景主要是底层系统编程、序列化、硬件地址操作等。普通业务代码应尽量避免使用。

cpp 复制代码
#include <cstdint>

int main() 
{
    int a = 42;
    int* p = &a;
    uintptr_t addr = reinterpret_cast<uintptr_t>(p); // 指针 -> 足够宽的整数
    int* q = reinterpret_cast<int*>(addr);           // 整数 -> 指针
    std::cout << "*q = " << *q << std::endl;

    double d = 3.14;
    // int* bad = reinterpret_cast<int*>(&d);        // 可以编译,但通过 bad 访问违反严格别名规则
    return 0;
}

reinterpret_cast的缺陷是不检查对齐、不检查对象真实类型,把整数随便转成指针再解引用很可能崩溃。

这个缺陷就导致用了reinterpret_cast的代码移植到其他平台时是一件很危险的事情,因为其结果依赖平台、指针宽度、对齐和对象表示,跨平台代码要非常谨慎。

const_cast

const_cast 最常用的用途是去除变量的 const 或 volatile 属性,方便在特定接口中传递指针或引用。它只改变类型系统中的 const/volatile 限定,不改变对象本身是否真的是 const。也就是说,如果原对象本身定义为 const,通过 const_cast 去掉 const 后修改它,仍然属于未定义行为。它适合用于"对象本身不是 const,只是当前引用/指针是 const"的场景。

错误用法:

cpp 复制代码
int main() 
{
    const int a = 2;
    int* p = const_cast<int*>(&a);   // 去掉 const
    *p = 3;                          // 未定义行为:a 本身是 const 对象
    std::cout << "a = " << a << ", *p = " << *p << std::endl;
    return 0;
}

正确用法:

cpp 复制代码
void oldApi(int* p) 
{
    *p = 100;
}

int main() 
{
    int x = 2;
    const int& cr = x;               // x 本身不是 const 对象
    oldApi(const_cast<int*>(&cr));   // 去掉的是引用带来的 const 限定
    std::cout << "x = " << x << std::endl;
    return 0;
}

为什么const_cast移除了const属性,对其修改仍是未定义行为?

  • 因为const属性是从该变量定义的时候就注定了它不能修改,const_cast只是去掉了const这个属性。比如:const int* 使用const_cast就变成了int* ,但是能否修改就看最初的变量是否是const属性的,如果不是就可以修改。因为const从定义开始对const属性变量修改操作就是未定义行为。

为什么C++要设计const_cast这个可以说没什么用的类型转换关键字?

  • const_cast,设计的初衷是对接缺 const 的老 API;就是以前的接口是有const属性的但是没写;
  • 第二个用法就是复用 const/非 const 重载的实现。

dynamic_cast

dynamic_cast 用于继承体系中的安全向下转型:把父类指针/引用转换为子类指针/引用,并在运行时检查实际对象类型是否匹配。向上转型(子类到父类)本来就可以赋值兼容,不需要 dynamic_cast;向下转型(父类到子类)使用 dynamic_cast 更安全。它要求父类至少含有一个虚函数,因为运行时类型识别依赖虚表等机制。指针转换失败返回 nullptr,引用转换失败抛出 std::bad_cast。

cpp 复制代码
class A 
{
public:
    virtual ~A() {}          // 有虚函数,支持多态和 dynamic_cast
};

class B : public A 
{
public:
    void hello() 
    {
        std::cout << "B::hello" << std::endl;
    }
};

void fun(A* pa) 
{
    B* pb1 = static_cast<B*>(pa);    // 不做运行时检查,可能得到非法指针
    B* pb2 = dynamic_cast<B*>(pa);   // 运行时检查,失败返回 nullptr
    std::cout << "pb1 = " << pb1 << ", pb2 = " << pb2 << std::endl;
    if (pb2 != nullptr) {
        pb2->hello();
    }
}

int main() 
{
    A a;
    B b;
    fun(&a);   // dynamic_cast 失败,pb2 为 nullptr
    fun(&b);   // dynamic_cast 成功
    return 0;
}

注意:

  • dynamic_cast 只能用于父类含有虚函数的类;如果父类没有虚函数,编译会失败。
  • static_cast 向下转型不做运行时检查,如果父类指针实际指向父类对象,强转后使用是未定义行为;正确做法是使用 dynamic_cast 并检查结果。
  • dynamic_cast 有运行时开销,不应滥用;如果设计上可以避免向下转型,优先使用虚函数和多态接口。

RTTI

RTTI 是 Run-Time Type Identification 的简称,即运行时类型识别。

C++通过以下方式来支持RTTI:

  1. typeid运算符
  2. dynamic_cast运算符
  3. decltype

typeid 运算符、dynamic_cast 运算符等机制支持运行时类型识别;

typeid 可以获取表达式的动态类型信息;

dynamic_cast 可以在运行时检查继承体系中的转换是否安全。需要注意;

decltype 是编译期类型推导工具,严格来说不属于运行时类型识别。

RTTI了解即可

总结:

需求 推荐方式 说明
基本数值类型转换 static_cast 明确表达静态数值转换意图,比C风格更清晰
void*转具体指针 static_cast 相关指针类型转换,编译期检查
指针与整数互转、底层位模式重解释 reinterpret_cast 仅在底层场景使用,必须确保地址有效、对齐正确
去除const/volatile限定 const_cast 确认原对象本身不是const,否则修改属于未定义行为
多态继承体系安全向下转型 dynamic_cast 父类需有虚函数;必须检查nullptr或捕获std::bad_cast
普通业务代码中的强制转换 优先避免,必要时用C++命名转换 C风格转换语义混合、可读性差,不推荐在新C++代码中使用
兼容C语言或旧代码 可保留C风格转换 C++兼容C风格,但新代码应优先使用命名转换

完

相关推荐
FLJwu3 小时前
运动相机图像传感器选型实战|第二篇:参数看着一模一样,为什么骑行录像疯狂果冻、拖影翻车?
android·c语言·开发语言·数码相机·运动相机·oem 量产
驭渊的小故事3 小时前
从 JVM 底层看泛型:泛型擦除、Signature 属性与 Super Type Token 模式
java·开发语言·jvm
Escalating_xu3 小时前
【C语言项目实战】贪吃蛇(下):移动、碰撞与计分,完成一局可玩的游戏
c语言·开发语言·游戏
火2253 小时前
# C++ 微服务即时通讯系统(三):网关设计——HTTP 请求、Session 鉴权与 WebSocket 通知上一篇
c++·http·微服务
qq_401700413 小时前
Qt Modbus 源码剖析:从 QModbusClient 到工业级通信框架
开发语言·qt·系统架构
星夜离尘563 小时前
MATLAB 角度解缠遇到丢样:先分段,再说明圈数为什么不可恢复
开发语言·matlab
我命由我123454 小时前
6-2.Python 异常处理 - 异常捕获(捕获指定异常、常见异常类型、异常信息)
java·运维·开发语言·python·http·https·java-ee
ZoeLandia4 小时前
React08:自定义 Hook
开发语言·前端·react.js·typescript
C++ 老炮儿的技术栈4 小时前
机器人的坐标,像 XYZ 的坐标值,是用单精度浮点还是用双精度浮点?
c语言·c++·人工智能·单片机·c