C/C++ 入门:文件 IO 流

程序跑完数据就没了,想”留到下次”就得把数据写进文件;文件 IO 流(file stream)就是 C++ 读写文件的通道。

📚 基本概念速读

名称 定义 省流
文件流(file stream) 把文件当成数据流的输入输出对象 读写文件的通道
ifstream 输入文件流,从文件读数据
ofstream 输出文件流,向文件写数据
fstream 可读可写的文件流 读写
文本文件 以字符形式存储,人能直接阅读 记事本能打开
二进制文件 按内存中的字节原样存储 紧凑、机器可读
序列化(serialization) 把对象转成可存储/传输的字节序列 对象存盘
反序列化(deserialization) 从字节序列恢复出对象 从盘恢复对象

🧩 文件流的基本模型

之前用的 coutcin 是标准输入输出流,数据通往控制台。文件流把数据的去向换成文件:程序通过流对象读写,流对象负责和文件打交道。



flowchart LR
    A[程序] --> B[ofstream 输出流]
    B --> C[文件]
    C --> D[ifstream 输入流]
    D --> A

流对象 头文件 作用
ifstream <fstream> 从文件读
ofstream <fstream> 向文件写
fstream <fstream> 既读又写

🛠️ 文件输入输出

写文件:ofstream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <fstream>
using namespace std;

int main()
{
// 打开文件准备写入(不存在则创建)
ofstream out("data.txt");

if (!out) {
cerr << "打开文件失败" << endl;
return 1;
}

out << "hello" << endl;
out << "world" << 42 << endl; // 和 cout 用法一样

out.close(); // 关闭文件
return 0;
}

ofstream 默认会覆盖已有文件内容。想追加写,加一个打开模式:

1
ofstream out("data.txt", ios::app);   // append:追加到末尾

读文件:ifstream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main()
{
ifstream in("data.txt");

if (!in) {
cerr << "打开文件失败" << endl;
return 1;
}

string word;
while (in >> word) { // 按空白分隔逐个读取
cout << word << endl;
}

in.close();
return 0;
}

close() 也可以不写:文件流对象析构时会自动关闭文件(RAII,第 9 章讲过)。但显式 close() 能让”文件何时关闭”更清晰,也方便在同一个作用域里重新打开别的文件。

常用打开模式

模式 含义
ios::in 读模式(ifstream 默认)
ios::out 写模式,覆盖(ofstream 默认)
ios::app 追加写
ios::binary 二进制模式
ios::trunc 打开即清空文件

📝 文本文件读写

逐行读取

getline 一次读一整行,适合按行处理配置文件、日志等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main()
{
ifstream in("data.txt");
if (!in) {
cerr << "打开文件失败" << endl;
return 1;
}

string line;
int lineNo = 1;
while (getline(in, line)) { // 读到一行就处理一行
cout << lineNo++ << ": " << line << endl;
}

return 0;
}

getline 读到文件末尾时返回假值,循环自然结束,不需要手动判断 EOF。

格式化写入

文本写入就是”把要保存的字段拼成可读文本”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <fstream>
using namespace std;

struct Student {
string name;
int age;
double score;
};

int main()
{
Student s = {"Alice", 18, 92.5};

ofstream out("students.txt");
if (!out) {
return 1;
}

// 每个字段用分隔符隔开,一行一个学生
out << s.name << "," << s.age << "," << s.score << endl;

return 0;
}

写出的文件内容:

1
Alice,18,92.5

反过来读回时,用 getline 按行读,再用 find/substr(第 10 章学过)按逗号拆字段。

🧱 二进制文件读写

基本用法

二进制文件按内存中的字节原样保存,紧凑且读写快,用 read/write 操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <fstream>
using namespace std;

struct Record {
int id;
double score;
};

int main()
{
Record r = {1001, 88.5};

// 写入:把 r 的内存字节直接写进文件
ofstream out("record.bin", ios::binary);
if (!out) {
return 1;
}
out.write(reinterpret_cast<char*>(&r), sizeof(r));
out.close();

// 读回
Record r2;
ifstream in("record.bin", ios::binary);
if (!in) {
return 1;
}
in.read(reinterpret_cast<char*>(&r2), sizeof(r2));

// r2.id == 1001, r2.score == 88.5
return 0;
}

write/read 的参数都是”字符指针 + 字节数”,所以结构体要先用 reinterpret_cast<char*> 转换。

重要限制

二进制直接读写只适合普通数据(POD)结构体——成员都是 intdouble、固定大小数组这类值类型。

1
2
3
4
struct Bad {
string name; // 内部有指针,直接 write 存的是地址不是内容
vector<int> v; // 同理
};

stringvector 内部管理堆内存,把整个对象字节写进文件,存进去的是”指针地址”,下次运行时地址早已失效。这类对象必须走序列化(下一节)。

数据 二进制直接读写 说明
intdouble、POD 结构体 可以 字节布局稳定
stringvector 不行 内部有指针,要序列化

💾 对象持久化

持久化(persistence)就是把对象保存到文件里,下次程序启动时再恢复。核心是序列化和反序列化。

字段序列化:文本方案

把对象的每个字段转成文本,逐字段保存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <fstream>
#include <string>
using namespace std;

struct Student {
string name;
int age;
double score;

// 序列化:写进文件
void save(ofstream& out) const
{
out << name << endl;
out << age << endl;
out << score << endl;
}

// 反序列化:从文件读回
bool load(ifstream& in)
{
if (!getline(in, name)) return false;
if (!(in >> age)) return false;
if (!(in >> score)) return false;
return true;
}
};
1
2
3
4
5
6
7
8
9
10
Student s = {"Alice", 18, 92.5};

ofstream out("student.txt");
s.save(out);
out.close();

Student t;
ifstream in("student.txt");
t.load(in);
// t 恢复了 name/age/score

版本兼容意识

文件格式一旦定下来,旧的存档和新的程序就可能对不上。写文件时建议带一个版本号

1
2
3
4
5
6
7
void save(ofstream& out) const
{
out << "v1" << endl; // 版本标记
out << name << endl;
out << age << endl;
out << score << endl;
}

以后格式变了(比如新增字段、改字段顺序),读的时候先看版本号,再做对应的解析。这是”老文件还能被新程序读”的基础意识。

序列化是很大的话题,工程里常用 JSON、protobuf 等现成格式。入门阶段先理解”把字段存下来、再读回来”这件事本身。

⚠️ 错误处理

打开失败

文件打开失败非常常见:路径写错、文件不存在、没有权限。打开后第一件事就是检查

1
2
3
4
5
ifstream in("data.txt");
if (!in) { // 或 !in.is_open()
cerr << "打开文件失败" << endl;
return 1;
}

读写状态

流对象内部记录着状态标志:

状态 含义
good() 一切正常
eof() 读到文件末尾(正常情况)
fail() 操作失败,比如类型不匹配、打开失败
bad() 严重错误,比如底层 IO 故障
1
2
3
4
5
6
int x;
if (in >> x) {
// 读取成功
} else {
// 读取失败或到达末尾
}

把”读取操作本身”当作判断条件,是最稳妥的写法。

路径问题

问题 建议
相对路径 相对的是程序的工作目录,不是可执行文件所在目录
绝对路径 最稳妥,但换机器要改
目录不存在 文件流不会自动创建目录,先建目录再打开
权限不足 只读文件用写模式打开会失败,先检查权限

⚠️ 常见误区

误区 正解
文件一定能打开成功 路径、权限、目录都可能失败,必须检查打开结果
string 可以直接二进制 write string 内部有指针,直接写存的是地址,要序列化
忘写 close() 会立刻出问题 析构会自动关闭,但显式 close() 更清晰
读到 EOF 是错误 eof() 是正常结束状态,读操作失败才需要处理
二进制数据用文本模式读写没关系 二进制文件要加 ios::binary,否则可能被换行符转换破坏
文件格式随便定,反正自己用 加版本号,为以后格式变更留余地

✅ 总结

文件流三件套各司其职:ifstream 读、ofstream 写、fstream 读写;文本文件逐行处理,二进制文件只适合 POD,对象持久化要序列化并带版本意识;任何文件操作先检查打开状态。

配合第 9 章的 RAII,文件流对象的自动关闭正好是 RAII 的又一次体现;配合第 10 章的 string/vector,文本解析也不再费力。

Happy Hacking! 🎉