除了和键盘、屏幕打交道,程序也可以直接读写磁盘上的文件。竞赛评测系统大多要求把输入输出重定向到文件,这是信息学竞赛的标配技能。
3.3
前面两节的输入输出都是和"人"打交道——从键盘读、往屏幕写。但很多时候,程序需要直接和"文件"打交道:从磁盘上的一个文件里读数据,或者把结果写进另一个文件保存下来。
在信息学竞赛中,几乎所有正式比赛(NOIP、CSP-J 等)都不是让你手动在键盘上敲输入、盯着屏幕看输出的——评测系统会把预先准备好的输入数据放进一个文件,要求你的程序从这个文件里读取数据,再把结果写进另一个指定的文件,由评测系统自动比对。这种"以文件作为输入输出"的模式,就是本节要学的内容。
C++ 提供了两种实现方式,各有各的适用场景:
cin/cout、scanf/printf 这些"熟悉的老朋友"直接对着文件读写,代码逻辑完全不用改。竞赛中最常用,几乎是标配写法。cin/cout 几乎一样,但可以同时打开多个文件、更灵活地控制文件操作。更通用,适合需要精细控制的场景。freopen 的思路很巧妙:它不改变你写 cin、cout、scanf、printf 的方式,而是在程序一开始,把"标准输入"和"标准输出"这两个默认通道,偷偷地"接上"两个文件。之后代码里所有的输入输出语句,实际读写的都是文件,而不是键盘和屏幕:
| 1 | #include <cstdio> |
| 2 | using namespace std; |
| 3 | |
| 4 | int 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 是竞赛中最常用的文件读写方式。如果需要同时操作多个文件(比如同时读两个输入文件),或者只是想在程序中间临时读写一个文件而不影响标准输入输出,就需要用到 <fstream> 头文件提供的 ifstream(输入文件流)和 ofstream(输出文件流)。它们的用法和 cin/cout 几乎一模一样,只是对象换成了你自己起的名字:
| 1 | #include <fstream> |
| 2 | using namespace std; |
| 3 | |
| 4 | int 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 | } |
cin、cout 不同,ifstream、ofstream 是"类型",fin、fout 只是你自己起的变量名(换成别的名字也完全可以)。这意味着你可以同时打开好几个输入文件、好几个输出文件,分别用不同的名字操作,这是 freopen 做不到的。如果指定的文件不存在,或者因为权限等原因打不开,ifstream/ofstream 并不会报错崩溃,而是静静地进入一种"打开失败"的状态,之后所有的读写操作都不会有任何效果。养成检查是否打开成功的习惯,能省下很多"明明代码没错,为什么什么都读不出来"的排查时间:
| 1 | ifstream fin("data.in"); |
| 2 | if (!fin) // fin 可以直接当作布尔值使用,! 表示"没打开成功" |
| 3 | { |
| 4 | cout << "文件打开失败!" << endl; |
| 5 | return 1; // 提前结束程序 |
| 6 | } |
从文件里读取不定量的数据时(比如不知道文件里到底有多少个数字),需要判断什么时候读到了文件末尾。最简洁、最不容易出错的写法,是直接把"读取"这个动作放进 while 的条件里:
| 1 | ifstream fin("data.in"); |
| 2 | int x, sum = 0; |
| 3 | while (fin >> x) // 推荐写法:读取成功才继续循环 |
| 4 | { |
| 5 | sum += x; // 把文件里所有数字累加起来 |
| 6 | } |
while (!fin.eof()):这是最常见的错误写法。eof() 只有在"尝试读取之后发现已经没有数据了"才会变成 true,也就是说它的状态总是慢一拍——用 eof() 判断会导致最后多循环一次,把上一次的旧数据重复处理一遍。正确做法是像上面代码那样,直接把 fin >> x 这个读取动作本身放进 while 的条件里:读取成功时结果为真,读取失败(包括到达文件末尾)时结果为假,一步到位,不会多算。从 scores.in 中读取若干个整数(数量不定),计算它们的总和与平均值,将结果写入 scores.out:
| 1 | #include <fstream> |
| 2 | #include <iomanip> |
| 3 | using namespace std; |
| 4 | |
| 5 | int 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 | } |
把本节内容汇总成几条最容易踩坑的规则:
while (fin >> x)。freopen("data.in", "r", stdin) 里的文件名默认是相对路径,指的是程序运行时所在的当前目录,而不一定是代码文件所在的目录。如果提示"文件打开失败",先检查文件是否真的放在了程序运行的目录下。ifstream/ofstream 不会报错,只会默默地"什么都读不到"或"什么都写不出",容易让人误以为是逻辑错误,白白花时间排查。养成 if (!fin) 检查的习惯。freopen 重定向到文件,提交到评测系统前,如果评测系统本身就是走标准输入输出(不需要 freopen),记得把这两行去掉或者根据比赛要求调整文件名,否则程序会因为找不到指定文件而运行失败。