第 29 章 存档读档——文件操作入门
🏗️ 前情回顾:内存地址都会操作了——指针让你在内存里想去哪就去哪。但程序一关,所有数据全没了。辛苦算了一下午的成绩排名,下次打开还得重新输入——太崩溃了。能不能像游戏存档一样,把数据存到文件里,下次打开接着用?这一章——文件操作入门,让程序拥有跨越时间的记忆力。
🎯 本章目标
学完这一章,你能:
- 用
freopen将标准输入输出重定向到文件 - 用
ifstream从文件读取数据 - 用
ofstream向文件写入数据 - 理解文本文件读写的完整流程(打开 → 读写 → 关闭)
- 写出信息学竞赛中标准的文件输入输出代码
- 检查文件是否成功打开并处理错误
📖 故事引入
小明写了一个日记程序,每天记录心情:
string mood;
cout << "今天心情怎么样?";
cin >> mood;
cout << "今天的日记:心情 " << mood << endl;程序运行得很好。但第二天打开程序——昨天的记录全没了!小明得重新输入。
这就像你打了一下午游戏,好不容易打到第 10 关——结果忘了存档,第二天打开,进度全没了,要从第 1 关重新打。😱
"能不能像游戏存档一样,写的东西存成一个文件,明天打开还能看到?"
当然可以。C++ 提供了两种读写的"通道":
- 屏幕 / 键盘:就是你已经用熟的
cin和cout - 文件:今天要学的
ifstream(读文件)和ofstream(写文件)
其实 cin 和 cout 本身就是特殊的"文件流"——只不过它们连的不是硬盘上的文件,而是控制台。文件操作就是把这个通道"拨"到硬盘上的某个文件去,就像游戏里点"保存进度"——啪,存档成功!
🧱 知识讲解
29.1 重定向:最快的"文件模式"
竞赛中最简单粗暴的文件操作是 freopen——把 stdin(标准输入)和 stdout(标准输出)重新定向到文件。从此,你用 cin 就是从文件读,用 cout 就是往文件写——代码完全不用改!
#include <cstdio> // freopen 需要这个头文件
using namespace std;
int main() {
freopen("input.txt", "r", stdin); // stdin 重定向到 input.txt(只读)
freopen("output.txt", "w", stdout); // stdout 重定向到 output.txt(只写)
// 下面的代码和平时完全一样!
int a, b;
cin >> a >> b; // 实际上从 input.txt 读
cout << a + b << endl; // 实际上写到 output.txt
return 0;
}| 参数 | 含义 |
|---|---|
"input.txt" | 文件名(和程序在同一目录,或用完整路径) |
"r" | 模式:"r" = 只读,"w" = 只写(会覆盖已有内容) |
stdin / stdout | 把标准输入/输出流重定向过去 |
优势:代码零改动!你写的所有 cin >> x 和 cout << x 自动变成文件读写。
局限:不够灵活——如果你想同时读写多个文件,或者部分输出到屏幕、部分写到文件,freopen 就做不到了。
💡 竞赛中经常要求"从
xxx.in读入,输出到xxx.out"。freopen就是为此而生的——两行代码搞定。
29.2 ifstream:从文件读取
如果你需要更精细的控制——比如同时打开两个文件、或者读文件的同时还能在屏幕上输出提示——用 ifstream(input file stream):
❌ 忘记检查文件是否打开——最常见的文件操作错误:
cppifstream fin("不存在的文件.txt"); // 文件不存在! int x; fin >> x; // ❌ 静默失败——x 的值是未定义的 cout << x; // 输出垃圾值,程序不报错,你都不知道出了问题铁律:打开文件后必须检查!
if (!fin.is_open())是第一道防线。
✅ 正确的文件读取流程(打开→检查→读取→关闭):
#include <fstream> // 文件流需要这个头文件
using namespace std;
int main() {
ifstream fin("data.txt"); // 打开 data.txt 准备读取
if (!fin.is_open()) { // 检查是否打开成功
cout << "文件打开失败!" << endl;
return 1;
}
int x;
while (fin >> x) { // 像 cin 一样用,读到文件末尾自动结束
cout << "读到:" << x << endl;
}
fin.close(); // 关闭文件(好习惯)
return 0;
}ifstream 的核心用法:
ifstream fin("文件名"); // 打开文件
fin >> 变量; // 从文件读(和 cin 语法一模一样)
fin.close(); // 关闭文件
fin.is_open(); // 判断是否打开成功
fin.eof(); // 判断是否读到文件末尾可以读任何类型——int、double、string、char,和 cin 完全一样的用法:
ifstream fin("students.txt");
string name;
int age;
double score;
fin >> name >> age >> score; // 从文件连续读入三个值29.3 ofstream:往文件写入
写文件和读文件对称——用 ofstream(output file stream):
#include <fstream>
using namespace std;
int main() {
ofstream fout("result.txt"); // 打开 result.txt 准备写入
if (!fout.is_open()) {
cout << "文件创建失败!" << endl;
return 1;
}
fout << "Hello, file!" << endl; // 向文件写入
fout << "这是第二行" << endl;
for (int i = 1; i <= 10; i++) {
fout << i << " "; // 写入数字
}
fout.close(); // 关闭文件(确保数据写入硬盘)
return 0;
}⚠️
ofstream打开文件时,默认会覆盖已有内容。如果想追加内容而不是覆盖,用ofstream fout("result.txt", ios::app);(app = append,追加模式)。
29.4 fstream:既能读又能写
fstream 是 ifstream 和 ofstream 的合体——同一个文件流既能读又能写(实践中较少用,但知道它存在):
#include <fstream>
fstream file("data.txt", ios::in | ios::out); // 可读可写竞赛中大多数情况,用 ifstream 读 + ofstream 写就够用了。
29.5 三种文件读写方式对比
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
freopen | 竞赛标准 I/O | 代码零改动,两行搞定 | 不灵活,不能同时多文件 |
ifstream / ofstream | 需要精细控制 | 灵活,可同时操作多个文件 | 需要用 fin >> 替代 cin >> |
fstream | 读写同一个文件 | 一体多用 | 竞赛中用得少 |
竞赛推荐写法——根据题目要求二选一:
// 方式一:freopen(题目说"从 xxx.in 读、输出到 xxx.out")
freopen("xxx.in", "r", stdin);
freopen("xxx.out", "w", stdout);
// 方式二:文件流(题目说"从文件读"但你也想在屏幕上看调试信息)
ifstream fin("xxx.in");
ofstream fout("xxx.out");
// 之后用 fin 代替 cin,fout 代替 cout29.6 文件打开失败检查
文件操作最容易出错的地方就是文件打不开——文件不存在、路径写错、没有权限……所以每次打开文件后都必须检查:
ifstream fin("data.txt");
if (!fin) { // 或者 !fin.is_open()
cout << "错误:无法打开文件 data.txt" << endl;
return 1; // 或 exit(1)
}竞赛中通常写得更简洁:
ifstream fin("xxx.in");
if (!fin.is_open()) {
return 1; // 快速失败
}养成检查的习惯,避免程序默默读了一堆垃圾数据还不知情。
29.7 完整示例:日记本程序
把本章开头的故事变成现实:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
string mood;
// 1. 先尝试读取上次存的日记
ifstream fin("diary.txt");
if (fin.is_open()) {
cout << "===== 上次的日记 =====" << endl;
string line;
while (getline(fin, line)) { // 逐行读取
cout << line << endl;
}
fin.close();
cout << "======================" << endl;
} else {
cout << "还没有日记记录。开始写第一篇吧!" << endl;
}
// 2. 写入新日记(追加模式——不覆盖旧内容)
ofstream fout("diary.txt", ios::app);
if (!fout.is_open()) {
cout << "无法写入日记文件!" << endl;
return 1;
}
cout << "今天心情怎么样?输入 mood:";
cin >> mood;
fout << "心情:" << mood << endl;
fout.close();
cout << "日记已保存!" << endl;
return 0;
}✋ 动手试试
试试 1:用 freopen 写一个程序,从 input.txt 读取两个整数,把它们的和输出到 output.txt。手动创建 input.txt 文件(在程序同目录下用记事本写上"10 20"),运行程序后查看 output.txt 的内容。
试试 2:用 ofstream 创建一个文件 numbers.txt,往里写入 1 到 100,每行一个数字。然后用 ifstream 重新打开这个文件,读取所有数字并求它们的总和。
试试 3:写一个程序,从 data.txt 读取一系列整数(每行一个),统计:元素个数、最大值、最小值、平均值(保留 2 位小数)。如果文件不存在,输出"文件未找到"并退出。
试试 4:用 ios::app 追加模式写一个"日志程序"。每次运行程序时,在 log.txt 末尾追加一行当前时间(用字符串代替即可,比如 "2025-01-01 12:00")和一条自定义消息。运行多次后查看文件内容。
🦶 你踩过这些坑吗
逐一核对,看你是不是也中过招:
- [ ] 忘记
#include <fstream>:用了ifstream但没写头文件 → ❌ 编译错误 → ✅ 文件流操作必须#include <fstream> - [ ]
freopen之后cin/cout不能交互:freopen("in.txt", "r", stdin);之后还想用键盘输入 → ❌ 键盘输入失效了 → ✅ 使用freopen后所有cin都从文件读,如需同时输出到屏幕,在freopen之前打印提示 - [ ] 写文件后忘记
close():写完文件不关闭 → ❌ 数据可能没有完全写入硬盘(还在缓冲区里)→ ✅ 用完文件后调用.close(),或利用 C++ 析构机制自动关闭 - [ ] 以写入模式打开已有文件,旧内容丢失:
ofstream fout("important.txt");→ ❌ 旧内容被清空!→ ✅ 想保留旧内容用追加模式:ofstream fout("important.txt", ios::app);
📝 练习
基础题
1. 填空题
(1)将 stdin 重定向到文件 "in.txt" 的代码是 freopen("in.txt", "\_\_\_\_", stdin);
(2)从文件读取的流类型是 ____,向文件写入的流类型是 ____。
(3)以追加模式打开文件,需要加参数 ____。
(4)文件流操作需要的头文件是 #include \_\_\_\_。
2. 判断题
(1)freopen 之后,cout 的输出会显示在屏幕上。( )
(2)ifstream fin("test.txt"); 如果文件不存在,fin.is_open() 返回 true。( )
(3)ofstream 默认以覆盖模式打开文件。( )
(4)文件流变量离开作用域时会自动关闭文件。( )
3. 选择题
(1)以下哪个模式用于追加写入文件?
A. ios::in B. ios::out C. ios::app D. ios::ate
(2)freopen 函数在哪个头文件中?
A. <iostream> B. <fstream> C. <cstdio> D. <string>
4. 改错题
下面代码想从 data.txt 读取整数并求和,请找出 2 处错误并改正:
int main() {
ifstream fin("data.txt");
int sum = 0, x;
while (fin >> x) sum += x;
fin.close();
cout << sum;
}提高题
5. 编程题 — 文件复制
写一个程序,将 source.txt 的内容逐行复制到 dest.txt 中。如果 source.txt 不存在,输出提示信息并退出。
6. 编程题 — 成绩统计(文件版)
文件 scores.txt 中每行存储一个学生的姓名和分数(空格分隔),如:
小明 96
小红 88
小刚 92写程序读取文件,输出:总人数、平均分(保留 2 位小数)、最高分和对应姓名、最低分和对应姓名。将统计结果同时输出到屏幕和文件 result.txt。
7. 编程题 — 数字文件求和
文件 numbers.txt 中每行一个整数。写程序读取所有整数,计算它们的总和与平均值(保留 2 位小数),将结果写入 result.txt。如果文件不存在,输出"文件未找到"。
挑战题
8. 编程题 — 文件合并与排序
有两个文件 a.txt 和 b.txt,每个文件都包含若干已排好序的整数(每行一个)。写程序将两个文件合并成一个新的有序文件 c.txt。
(提示:用两个 ifstream 同时读取,每次比较两个文件当前读到的数,把较小的写入 c.txt。这就是"归并"操作。)
9. 编程题 — CSV 解析器
CSV(Comma-Separated Values)是一种用逗号分隔字段的数据格式。文件 data.csv 内容如下:
姓名,年龄,分数
小明,13,96
小红,14,88
小刚,13,92写程序读取这个文件,跳过第一行(表头),计算并输出所有学生的平均年龄和平均分数(保留 2 位小数)。
(提示:使用 getline(fin, line) 逐行读取,然后解析每行中用逗号分隔的三个字段。可以用 string 的 find 和 substr 来拆分,或者用 stringstream。)
🧠 本章小结
文件操作三件套:
1. freopen(重定向——竞赛最爱)
freopen("in.txt", "r", stdin); // cin 现在从文件读
freopen("out.txt", "w", stdout); // cout 现在往文件写
2. ifstream(读文件)
ifstream fin("文件名");
fin >> 变量; // 和 cin 一样用
fin.is_open(); // 检查是否打开成功
fin.close(); // 关闭
3. ofstream(写文件)
ofstream fout("文件名");
fout << 内容; // 和 cout 一样用
fout.close(); // 关闭
追加模式:ofstream fout("文件名", ios::app);
头文件:#include <fstream>📝 配套练习
共3题。文件操作本质是"把cin/cout换成文件"——用旧题改I/O方式即可。。★核心(课堂必做) ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0114 | https://hydro.ac/d/srqc/p/J0114 | struct+sort+文件I/O |
| ◆ 拓展 | 复用J0106 | — | 同样的题,改成freopen版本 |
| ◆ 拓展 | 复用J0103 | — | 歌唱比赛,改成文件读写 |
💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 2 道◆拓展题,覆盖不同变式和细节。
配套练习
共3题。文件操作本质是"把cin/cout换成文件"——用旧题改I/O方式即可。★核心(课堂必做) · ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0114 | https://hydro.ac/d/srqc/p/J0114 | struct+sort+文件I/O |
| ◆ 拓展 | 复用J0106 | — | 同样的题,改成freopen版本 |
| ◆ 拓展 | 复用J0103 | — | 歌唱比赛,改成文件读写 |
练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 2 道◆拓展题,覆盖不同变式和细节。
自查清单:
- [ ] 我会用
freopen重定向标准输入输出到文件 - [ ] 我知道
freopen的优点(代码不改)和局限(不灵活) - [ ] 我会用
ifstream从文件读取各种类型的数据 - [ ] 我会用
ofstream向文件写入数据 - [ ] 我知道如何检查文件是否成功打开
- [ ] 我知道覆盖模式和追加模式的区别
- [ ] 我会用
ios::app在文件末尾追加内容
🚀 下章预告
恭喜你完成了 M4 模块的学习!从函数封装到参数传递,从作用域到递归,从结构体到指针,再到文件读写——你已经掌握了 C++ 核心编程的全部基本功。在 M5 模块中,我们将进入算法世界的第一步:复杂度分析与基础算法——开始真正像竞赛选手一样思考问题!🎉