← 目录 / 第三章 · 输入与输出 / 3.3 文件读写

3.3 文件读写

除了和键盘、屏幕打交道,程序也可以直接读写磁盘上的文件。竞赛评测系统大多要求把输入输出重定向到文件,这是信息学竞赛的标配技能。

文件读写

前面两节的输入输出都是和"人"打交道——从键盘读、往屏幕写。但很多时候,程序需要直接和"文件"打交道:从磁盘上的一个文件里读数据,或者把结果写进另一个文件保存下来。

3.3.1 为什么需要文件读写

在信息学竞赛中,几乎所有正式比赛(NOIP、CSP-J 等)都不是让你手动在键盘上敲输入、盯着屏幕看输出的——评测系统会把预先准备好的输入数据放进一个文件,要求你的程序从这个文件里读取数据,再把结果写进另一个指定的文件,由评测系统自动比对。这种"以文件作为输入输出"的模式,就是本节要学的内容。

C++ 提供了两种实现方式,各有各的适用场景:

🔀 freopen 重定向
cin/coutscanf/printf 这些"熟悉的老朋友"直接对着文件读写,代码逻辑完全不用改。竞赛中最常用,几乎是标配写法。
📂 ifstream / ofstream
专门用来读写文件的流对象,语法和 cin/cout 几乎一样,但可以同时打开多个文件、更灵活地控制文件操作。更通用,适合需要精细控制的场景。

3.3.2 freopen:竞赛标配的输入输出重定向

freopen 的思路很巧妙:它不改变你写 cincoutscanfprintf 的方式,而是在程序一开始,把"标准输入"和"标准输出"这两个默认通道,偷偷地"接上"两个文件。之后代码里所有的输入输出语句,实际读写的都是文件,而不是键盘和屏幕:

cin / scanf
标准输入 stdin
📄 data.in
cout / printf
标准输出 stdout
📄 data.out
C++ · freopen 重定向模板
1#include <cstdio>
2using namespace std;
3
4int main()
5{
6 freopen("data.in", "r", stdin); // 把标准输入接到 data.in
7 freopen("data.out", "w", stdout); // 把标准输出接到 data.out
8
9 int a, b;
10 cin >> a >> b; // 实际是从 data.in 里读,不是键盘
11 cout << a + b << endl; // 实际是写进 data.out,不是屏幕
12 return 0;
13}

freopen 的三个参数含义是:freopen(文件名, 打开模式, 要重定向的流)。打开模式最常用的是 "r"(只读,用于输入文件)和 "w"(只写,用于输出文件,如果文件已存在会被清空重写)。

🏆
竞赛习惯:正式比赛通常会明确要求输入输出文件名,比如"从 sort.in 读入,输出到 sort.out"。这时只需要在 main() 开头加上两行 freopen,后面的代码可以和平时用键盘调试时完全一样,不需要做任何改动——这也是为什么 freopen 是竞赛中最常用的文件读写方式。

3.3.3 ifstream / ofstream:更通用的文件流

如果需要同时操作多个文件(比如同时读两个输入文件),或者只是想在程序中间临时读写一个文件而不影响标准输入输出,就需要用到 <fstream> 头文件提供的 ifstream(输入文件流)和 ofstream(输出文件流)。它们的用法和 cin/cout 几乎一模一样,只是对象换成了你自己起的名字:

C++ · ifstream / ofstream 基本用法
1#include <fstream>
2using namespace std;
3
4int main()
5{
6 ifstream fin("data.in"); // 打开输入文件,命名为 fin
7 ofstream fout("data.out"); // 打开输出文件,命名为 fout
8
9 int a, b;
10 fin >> a >> b; // 像 cin 一样读,只是对象换成了 fin
11 fout << a + b << endl; // 像 cout 一样写,对象换成了 fout
12
13 fin.close(); // 用完关闭文件(好习惯)
14 fout.close();
15 return 0;
16}
📖
fin / fout 只是变量名:cincout 不同,ifstreamofstream 是"类型",finfout 只是你自己起的变量名(换成别的名字也完全可以)。这意味着你可以同时打开好几个输入文件、好几个输出文件,分别用不同的名字操作,这是 freopen 做不到的。

3.3.4 判断文件是否成功打开

如果指定的文件不存在,或者因为权限等原因打不开,ifstream/ofstream 并不会报错崩溃,而是静静地进入一种"打开失败"的状态,之后所有的读写操作都不会有任何效果。养成检查是否打开成功的习惯,能省下很多"明明代码没错,为什么什么都读不出来"的排查时间:

C++ · 检查文件是否打开成功
1ifstream fin("data.in");
2if (!fin) // fin 可以直接当作布尔值使用,! 表示"没打开成功"
3{
4 cout << "文件打开失败!" << endl;
5 return 1; // 提前结束程序
6}

3.3.5 判断文件读取是否结束

从文件里读取不定量的数据时(比如不知道文件里到底有多少个数字),需要判断什么时候读到了文件末尾。最简洁、最不容易出错的写法,是直接把"读取"这个动作放进 while 的条件里:

C++ · 循环读取直到文件结束
1ifstream fin("data.in");
2int x, sum = 0;
3while (fin >> x) // 推荐写法:读取成功才继续循环
4{
5 sum += x; // 把文件里所有数字累加起来
6}
🚫
不要用 while (!fin.eof())这是最常见的错误写法。eof() 只有在"尝试读取之后发现已经没有数据了"才会变成 true,也就是说它的状态总是慢一拍——用 eof() 判断会导致最后多循环一次,把上一次的旧数据重复处理一遍。正确做法是像上面代码那样,直接把 fin >> x 这个读取动作本身放进 while 的条件里:读取成功时结果为真,读取失败(包括到达文件末尾)时结果为假,一步到位,不会多算。

3.3.6 综合示例:从文件读取并写入结果

scores.in 中读取若干个整数(数量不定),计算它们的总和与平均值,将结果写入 scores.out

C++ · 文件读写综合示例
1#include <fstream>
2#include <iomanip>
3using namespace std;
4
5int main()
6{
7 ifstream fin("scores.in");
8 if (!fin) return 1; // 打开失败就直接结束
9
10 int x, sum = 0, count = 0;
11 while (fin >> x)
12 {
13 sum += x;
14 count++;
15 }
16 fin.close();
17
18 ofstream fout("scores.out");
19 fout << "总和:" << sum << endl;
20 fout << "平均值:" << fixed << setprecision(2)
21 << (double)sum / count << endl; // 见 2.1 节:先转 double 再除
22 fout.close();
23 return 0;
24}

3.3.7 常见陷阱与易错点

把本节内容汇总成几条最容易踩坑的规则:

1
不要用 while(!fin.eof()) 判断结束:会导致最后一组数据被重复处理一次。始终使用"把读取动作放进 while 条件"的写法,即 while (fin >> x)
2
文件路径问题:freopen("data.in", "r", stdin) 里的文件名默认是相对路径,指的是程序运行时所在的当前目录,而不一定是代码文件所在的目录。如果提示"文件打开失败",先检查文件是否真的放在了程序运行的目录下。
3
忘记检查是否打开成功:文件不存在时 ifstream/ofstream 不会报错,只会默默地"什么都读不到"或"什么都写不出",容易让人误以为是逻辑错误,白白花时间排查。养成 if (!fin) 检查的习惯。
4
用 freopen 调试时忘记切回键盘输入:本地测试时如果用了 freopen 重定向到文件,提交到评测系统前,如果评测系统本身就是走标准输入输出(不需要 freopen),记得把这两行去掉或者根据比赛要求调整文件名,否则程序会因为找不到指定文件而运行失败。