C/C++ 入门:文件 IO 流
程序跑完数据就没了,想”留到下次”就得把数据写进文件;文件 IO 流(file stream)就是 C++ 读写文件的通道。
📚 基本概念速读
| 名称 | 定义 | 省流 |
|---|---|---|
| 文件流(file stream) | 把文件当成数据流的输入输出对象 | 读写文件的通道 |
ifstream |
输入文件流,从文件读数据 | 读 |
ofstream |
输出文件流,向文件写数据 | 写 |
fstream |
可读可写的文件流 | 读写 |
| 文本文件 | 以字符形式存储,人能直接阅读 | 记事本能打开 |
| 二进制文件 | 按内存中的字节原样存储 | 紧凑、机器可读 |
| 序列化(serialization) | 把对象转成可存储/传输的字节序列 | 对象存盘 |
| 反序列化(deserialization) | 从字节序列恢复出对象 | 从盘恢复对象 |
🧩 文件流的基本模型
之前用的 cout、cin
是标准输入输出流,数据通往控制台。文件流把数据的去向换成文件:程序通过流对象读写,流对象负责和文件打交道。
flowchart LR
A[程序] --> B[ofstream 输出流]
B --> C[文件]
C --> D[ifstream 输入流]
D --> A
| 流对象 | 头文件 | 作用 |
|---|---|---|
ifstream |
<fstream> |
从文件读 |
ofstream |
<fstream> |
向文件写 |
fstream |
<fstream> |
既读又写 |
🛠️ 文件输入输出
写文件:ofstream
1 |
|
ofstream
默认会覆盖已有文件内容。想追加写,加一个打开模式:
1 | ofstream out("data.txt", ios::app); // append:追加到末尾 |
读文件:ifstream
1 |
|
close()也可以不写:文件流对象析构时会自动关闭文件(RAII,第 9 章讲过)。但显式close()能让”文件何时关闭”更清晰,也方便在同一个作用域里重新打开别的文件。
常用打开模式
| 模式 | 含义 |
|---|---|
ios::in |
读模式(ifstream 默认) |
ios::out |
写模式,覆盖(ofstream 默认) |
ios::app |
追加写 |
ios::binary |
二进制模式 |
ios::trunc |
打开即清空文件 |
📝 文本文件读写
逐行读取
getline 一次读一整行,适合按行处理配置文件、日志等:
1 |
|
getline
读到文件末尾时返回假值,循环自然结束,不需要手动判断 EOF。
格式化写入
文本写入就是”把要保存的字段拼成可读文本”:
1 |
|
写出的文件内容:
1 | Alice,18,92.5 |
反过来读回时,用 getline 按行读,再用
find/substr(第 10 章学过)按逗号拆字段。
🧱 二进制文件读写
基本用法
二进制文件按内存中的字节原样保存,紧凑且读写快,用
read/write 操作:
1 |
|
write/read 的参数都是”字符指针 +
字节数”,所以结构体要先用 reinterpret_cast<char*>
转换。
重要限制
二进制直接读写只适合普通数据(POD)结构体——成员都是
int、double、固定大小数组这类值类型。
1 | struct Bad { |
string、vector
内部管理堆内存,把整个对象字节写进文件,存进去的是”指针地址”,下次运行时地址早已失效。这类对象必须走序列化(下一节)。
| 数据 | 二进制直接读写 | 说明 |
|---|---|---|
int、double、POD 结构体 |
可以 | 字节布局稳定 |
string、vector |
不行 | 内部有指针,要序列化 |
💾 对象持久化
持久化(persistence)就是把对象保存到文件里,下次程序启动时再恢复。核心是序列化和反序列化。
字段序列化:文本方案
把对象的每个字段转成文本,逐字段保存:
1 |
|
1 | Student s = {"Alice", 18, 92.5}; |
版本兼容意识
文件格式一旦定下来,旧的存档和新的程序就可能对不上。写文件时建议带一个版本号:
1 | void save(ofstream& out) const |
以后格式变了(比如新增字段、改字段顺序),读的时候先看版本号,再做对应的解析。这是”老文件还能被新程序读”的基础意识。
序列化是很大的话题,工程里常用 JSON、protobuf 等现成格式。入门阶段先理解”把字段存下来、再读回来”这件事本身。
⚠️ 错误处理
打开失败
文件打开失败非常常见:路径写错、文件不存在、没有权限。打开后第一件事就是检查:
1 | ifstream in("data.txt"); |
读写状态
流对象内部记录着状态标志:
| 状态 | 含义 |
|---|---|
good() |
一切正常 |
eof() |
读到文件末尾(正常情况) |
fail() |
操作失败,比如类型不匹配、打开失败 |
bad() |
严重错误,比如底层 IO 故障 |
1 | int x; |
把”读取操作本身”当作判断条件,是最稳妥的写法。
路径问题
| 问题 | 建议 |
|---|---|
| 相对路径 | 相对的是程序的工作目录,不是可执行文件所在目录 |
| 绝对路径 | 最稳妥,但换机器要改 |
| 目录不存在 | 文件流不会自动创建目录,先建目录再打开 |
| 权限不足 | 只读文件用写模式打开会失败,先检查权限 |
⚠️ 常见误区
| 误区 | 正解 |
|---|---|
| 文件一定能打开成功 | 路径、权限、目录都可能失败,必须检查打开结果 |
string 可以直接二进制 write |
string 内部有指针,直接写存的是地址,要序列化 |
忘写 close() 会立刻出问题 |
析构会自动关闭,但显式 close() 更清晰 |
| 读到 EOF 是错误 | eof() 是正常结束状态,读操作失败才需要处理 |
| 二进制数据用文本模式读写没关系 | 二进制文件要加
ios::binary,否则可能被换行符转换破坏 |
| 文件格式随便定,反正自己用 | 加版本号,为以后格式变更留余地 |
✅ 总结
文件流三件套各司其职:
ifstream读、ofstream写、fstream读写;文本文件逐行处理,二进制文件只适合 POD,对象持久化要序列化并带版本意识;任何文件操作先检查打开状态。
配合第 9 章的 RAII,文件流对象的自动关闭正好是 RAII
的又一次体现;配合第 10 章的
string/vector,文本解析也不再费力。
Happy Hacking! 🎉