☰
C++学习/复习33类型转换与IO流
2026/10/5 10:37:14 网站建设 项目流程

一、C/C++ 类型转换

1.1 C类型转换及缺陷

C 语言提供两种类型转换方式:

隐式类型转换(编译器自动完成)和显式强制类型转换((type)expr 形式)

隐式类型转换

编译器在以下场景自动进行隐式转换:

算术运算中的类型提升(如 int + double → double)

赋值时的类型转换(如 double d = 1;)

函数传参时的类型匹配

数组名退化为指针、非 0 指针转 bool 等

显式强制类型转换

uintptr_t 能保证整数足够大、完整容纳指针值,不会截断

C 风格转换的缺陷

缺陷1:转换可视性差。所有转换都以 (type)expr,同一种形式书写,难以区分"安全的数值转换"和"危险的指针重解释",代码审查时难以跟踪错误转换。

缺陷2:隐式转换可能导致精度丢失或数据截断。如 double → int截断小数部分,有符号/无符号混合比较时的隐式转换可能产生意外结果。

缺陷3:显式转换将所有情况混合在一起。(int*)p 既可能是合法的 void* 转换,也可能是危险的不相关类型指针转换,编译器无法做针对性检查。

缺陷4:难以搜索和定位。在大型代码库中,(type) 形式的转换难以用 grep 精确查找。

C++ 为兼容 C 语言,仍然允许使用 C 风格转换,但 C++ Core Guidelines 强烈建议避免使用

1.2 C++四种强制类型转换

C++ 将类型转换细分为四种命名运算符,每种负责一类语义明确的转换场景,这种设计的优势在于各司其职、意图明确、编译器能做更多安全检查、代码可读性极高。每种转换运算符的名称本身就说明了转换的语义和风险等级

static_cast —— 最常用的静态转换

最常用、最安全的一种,在编译期完成类型检查,无运行时开销;它替代了大部分C风格转换,且编译期检查能捕获明显的类型错误

合法用途:

① 基本类型转换:int double、int char、enum int 等

② 向上转型(子类指针/引用 → 父类指针/引用):安全,无需转换也可隐式完成

③向下转型(父类指针/引用 → 子类指针/引用):允许但不检查,存在风险

④ void* 与具体类型指针互转

⑤ 显式调用单参数构造函数进行转换

不能做的事:

static_cast 不能在多态类层次中做安全向下转型(无运行时检查),需用 dynamic_cast

static_cast 不能用于两个不相关类型的指针互转(如 int* → double*),编译报错

static_cast 不能去除 const/volatile 限定符,需用 const_cast

dynamic_cast —— 唯一的运行时检查转换

唯一在运行期进行类型检查的运算符,专门用于多态继承结构中的安全向下转型

前提条件:

目标类型(父类)必须含有虚函数(通常是虚析构函数),否则编译报错

转换失败的行为:

性能注意:dynamic_cast 依赖RTTI,有运行时开销。在性能敏感的热路径中应谨慎使用,可考虑虚函数多态替代向下转型

const_cast —— 常量限定符的增与删

用于修改const/volatile限定符的运算符,不能改变变量的基础类型

核心规则

① 只能增删 const/volatile,不能改变基础类型(如 int* → double* 编译报错)

② 可用于指针、引用、this 指针,不能直接转换普通变量

③ 操作的对象本身必须是非 const 的

安全用法 vs 危险用法

对原生 const 变量去除 const 后修改,是未定义行为(UB)

编译器可能将 const变量放入只读内存段,修改会触发段错误;也可能因常量传播优化导致读取到的值仍是旧值

实战场景:const 成员函数

const 成员函数中,this 指针的类型是 const 类名*,默认不能修改任何成员变量。

const_cast 可去除 this 的 const 属性,但前提是对象本身非 const

优雅替代:mutable 关键字

对于缓存、计数器、互斥锁等需要在 const 函数中修改的内部成员,优先使用 mutable,而非 const_cast。

mutable 明确表达了"该成员不受 const 约束"的语义

安全使用总结表

reinterpret_cast —— 底层二进制重解释

四种转换中最暴力、最危险、最低级的一种,直接对操作数的二进制比特位重新解释,几乎无安全检查,结果不可移植。

典型用途

① 指针整数(将指针值存储为整数)

② 无关类型指针互转(如 int * 转为 char* ,用于逐字节访问)

③ 函数指针转换(不同类型函数指针互转)

④ 指针到指针的底层重解释(如 uintptr_t 操作)

int a = 0x12345678; char* p = reinterpret_cast<char*>(&a); for (size_t i = 0; i < sizeof(a); ++i) { cout << hex << (static_cast<unsigned>(p[i]) & 0xFF) << " "; }

将 char 转换为无符号整数并掩码处理,确保输出的是该字节的十六进制值,避免符号扩展带来的问题(例如,如果 char 是有符号的且最高位为 1,直接转换可能会得到负数或较大的值)

在‌小端机器‌上输出为:78 56 34 12

reinterpret_cast 的结果依赖于平台的内存布局、字节序、对齐方式等,同一份代码在不同架构 上可能产生不同结果

仅应用于极少数底层编程场景,如硬件寄存器访问、序列化框架的底层实现等

日常开发中应避免 reinterpret_cast。如果需要逐字节访问对象,应使用 unsigned char* (标准允许通过 char/unsigned char 指针访问任何对象的字节表示),且转换可用 static_cast 完成

1.3 RTTI:运行时类型识别

RTTI(Run-Time Type Identification)允许程序在运行时获取对象的实际类型信息

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

①typeid运算符:获取类型的std::type_info对象

② dynamic_cast 运算符:基于 RTTI 实现安全向下转型

③ decltype:编译期类型推导(严格来说不属于 RTTI,但常一起讨论)

示例

#include <iostream> #include <typeinfo> #include <cxxabi.h> // 用于 GCC/Clang 解码类型名 // 辅助函数:获取可读类型名 std::string get_readable_name(const std::type_info& ti) { int status = 0; char* demangled = abi::__cxa_demangle(ti.name(), nullptr, nullptr, &status); if (status == 0 && demangled) { std::string result(demangled); free(demangled); return result; } return ti.name(); // 失败或 MSVC 直接返回原名 } class Base { public: virtual ~Base() {} // 关键:必须有虚函数以支持多态 }; class Derived : public Base { public: void specific_func() { std::cout << "Derived specific function called." << std::endl; } }; int main() { Base* pb = new Derived; // 1. 安全检查 if (pb) { // 2. 输出类型名称 std::cout << "Type name: " << get_readable_name(typeid(*pb)) << std::endl; // 3. 类型判断 if (typeid(*pb) == typeid(Derived)) { std::cout << "确认:pb 指向的是 Derived 对象" << std::endl; // 4. 安全转型并调用特有方法 Derived* pd = static_cast<Derived*>(pb); // 既然已通过 typeid 确认,static_cast 即可 pd->specific_func(); } } delete pb; return 0; }

关键注意事项:

1 编译器必须启用 RTTI

typeid 的 name() 返回的字符串是编译器实现定义的,不同编译器输出格式不同,不应用于逻辑判断

2 typeid(ptr) vs typeid(*ptr)

3 空指针风险

4 性能考量

二、C++ IO 流

2.1 C 语言 IO 与缓冲区机制

C 语言通过 <cstdio>(原 <stdio.h>)提供标准 IO 函数,最常用的是 scanf() 和 printf()

C 语言的 IO 借助缓冲区来协调低速设备与高速 CPU 之间的速度差

缓冲区的作用

①屏蔽低级 I/O 实现差异:低级 I/O依赖操作系统内核实现,缓冲区层可屏蔽这些差异,便于写出可移植程序

②实现"行"读取的行为:计算机本身没有"行"的概念,缓冲区可以定义"行"的概念,解析缓冲区内容后 返回一个"行"

③减少系统调用次数:批量读写比逐字节读写效率高得多

C 风格常用 IO 函数

sprintf 的安全隐患:sprintf 不检查目标缓冲区大小,可能导致缓冲区溢出

应优先使用 snprintf(指定缓冲区大小)

2.2 C++ IO 流体系架构

C++ 的 IO 发生在流(stream)中,流是字节序列。从设备流向内存称为输入操作,从内存流向设备称为输出操作

C++ 标准库将 IO 组织为面向对象的类层次结构

类层次结构

ios_base 是所有流类的根基类,定义了流状态(iostate)、打开模式(openmode)等通用枚举。ios 继承自 ios_base,持有一个 streambuf 指针。istream 和 ostream 继承自 ios,分别提供输入和输出操作。iostream 多重继承自 istream 和 ostream。

底层的 streambuf 负责实际的缓冲区管理和字符传输,filebuf 和 stringbuf 分别是文件和字符串的 streambuf 实现。这种"流类 + 缓冲区类"的分离设计是 C++ IO 流的核心架构。

IO 流分类

2.3 流的四种状态(iostate)

IO 操作可能发生错误,C++ 标准库用 iostate 表示流的状态,定义在 ios_base 中。不同编译器的 iostate 实现可能不同,但都包含以下四种状态

状态查询函数

流状态恢复

当流进入 failbit 状态后,后续 IO 操作会全部失败。需要通过clear() 重置状态位,并通过 ignore() 清空缓冲区中的错误数据,才能继续使用

C++ 中处理标准输入流错误:

在交互式控制台输入中,用 !cin.good() 或 cin.fail() 来捕获格式错误是常见的;上述代码主要针对 failbit(格式错误)

如果 cin.bad() 为真(表示底层 I/O 错误,如磁盘损坏或连接断开),通常无法通过 clear() 和 ignore() 恢复,程序应终止或进行更高级的错误处理。

2.4 标准 IO 流

C++ 标准库定义了四个预定义的全局流对象:

cin 的工作机制

① 行缓冲:键盘输入的数据保存在缓冲区中,按回车键后才送入输入缓冲区

② 提取运算符 >>:默认跳过空格、Tab、换行符等空白字符,以它们为分隔符提取数据

③ 返回值:cin >> expr 返回 cin 对象本身,因此可以链式调用(cin >> num1 >> num2)

④ 布尔转换:cin 对象可隐式转换为 bool,流状态非 fail/bad 时返回 true,比如 if (cin >> x)

getline与get

cin >> 以空白字符为分隔符,无法读取包含空格的字符串。需要读取整行时使用 getline()

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询