Skip to content

第 29 章 存档读档——文件操作入门


🏗️ 前情回顾:内存地址都会操作了——指针让你在内存里想去哪就去哪。但程序一关,所有数据全没了。辛苦算了一下午的成绩排名,下次打开还得重新输入——太崩溃了。能不能像游戏存档一样,把数据存到文件里,下次打开接着用?这一章——文件操作入门,让程序拥有跨越时间的记忆力。

🎯 本章目标

学完这一章,你能:

  • freopen 将标准输入输出重定向到文件
  • ifstream 从文件读取数据
  • ofstream 向文件写入数据
  • 理解文本文件读写的完整流程(打开 → 读写 → 关闭)
  • 写出信息学竞赛中标准的文件输入输出代码
  • 检查文件是否成功打开并处理错误

📖 故事引入

小明写了一个日记程序,每天记录心情:

cpp
string mood;
cout << "今天心情怎么样?";
cin >> mood;
cout << "今天的日记:心情 " << mood << endl;

程序运行得很好。但第二天打开程序——昨天的记录全没了!小明得重新输入。

这就像你打了一下午游戏,好不容易打到第 10 关——结果忘了存档,第二天打开,进度全没了,要从第 1 关重新打。😱

"能不能像游戏存档一样,写的东西存成一个文件,明天打开还能看到?"

当然可以。C++ 提供了两种读写的"通道":

  • 屏幕 / 键盘:就是你已经用熟的 cincout
  • 文件:今天要学的 ifstream(读文件)和 ofstream(写文件)

其实 cincout 本身就是特殊的"文件流"——只不过它们连的不是硬盘上的文件,而是控制台。文件操作就是把这个通道"拨"到硬盘上的某个文件去,就像游戏里点"保存进度"——啪,存档成功!


🧱 知识讲解

29.1 重定向:最快的"文件模式"

竞赛中最简单粗暴的文件操作是 freopen——把 stdin(标准输入)和 stdout(标准输出)重新定向到文件。从此,你用 cin 就是从文件读,用 cout 就是往文件写——代码完全不用改!

cpp
#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 >> xcout << x 自动变成文件读写。

局限:不够灵活——如果你想同时读写多个文件,或者部分输出到屏幕、部分写到文件,freopen 就做不到了。

💡 竞赛中经常要求"从 xxx.in 读入,输出到 xxx.out"。freopen 就是为此而生的——两行代码搞定。

29.2 ifstream:从文件读取

如果你需要更精细的控制——比如同时打开两个文件、或者读文件的同时还能在屏幕上输出提示——用 ifstream(input file stream):

忘记检查文件是否打开——最常见的文件操作错误:

cpp
ifstream fin("不存在的文件.txt");  // 文件不存在!
int x;
fin >> x;         // ❌ 静默失败——x 的值是未定义的
cout << x;         // 输出垃圾值,程序不报错,你都不知道出了问题

铁律:打开文件后必须检查! if (!fin.is_open()) 是第一道防线。

✅ 正确的文件读取流程(打开→检查→读取→关闭):

cpp
#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 的核心用法:

cpp
ifstream fin("文件名");     // 打开文件
fin >> 变量;                // 从文件读(和 cin 语法一模一样)
fin.close();                // 关闭文件
fin.is_open();              // 判断是否打开成功
fin.eof();                  // 判断是否读到文件末尾

可以读任何类型——intdoublestringchar,和 cin 完全一样的用法:

cpp
ifstream fin("students.txt");
string name;
int age;
double score;

fin >> name >> age >> score;  // 从文件连续读入三个值

29.3 ofstream:往文件写入

写文件和读文件对称——用 ofstream(output file stream):

cpp
#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:既能读又能写

fstreamifstreamofstream 的合体——同一个文件流既能读又能写(实践中较少用,但知道它存在):

cpp
#include <fstream>
fstream file("data.txt", ios::in | ios::out);  // 可读可写

竞赛中大多数情况,用 ifstream 读 + ofstream 写就够用了。

29.5 三种文件读写方式对比

方式适用场景优点缺点
freopen竞赛标准 I/O代码零改动,两行搞定不灵活,不能同时多文件
ifstream / ofstream需要精细控制灵活,可同时操作多个文件需要用 fin >> 替代 cin >>
fstream读写同一个文件一体多用竞赛中用得少

竞赛推荐写法——根据题目要求二选一:

cpp
// 方式一: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 代替 cout

29.6 文件打开失败检查

文件操作最容易出错的地方就是文件打不开——文件不存在、路径写错、没有权限……所以每次打开文件后都必须检查

cpp
ifstream fin("data.txt");
if (!fin) {  // 或者 !fin.is_open()
    cout << "错误:无法打开文件 data.txt" << endl;
    return 1;  // 或 exit(1)
}

竞赛中通常写得更简洁:

cpp
ifstream fin("xxx.in");
if (!fin.is_open()) {
    return 1;  // 快速失败
}

养成检查的习惯,避免程序默默读了一堆垃圾数据还不知情。

29.7 完整示例:日记本程序

把本章开头的故事变成现实:

cpp
#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 处错误并改正:

cpp
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.txtb.txt,每个文件都包含若干已排好序的整数(每行一个)。写程序将两个文件合并成一个新的有序文件 c.txt

(提示:用两个 ifstream 同时读取,每次比较两个文件当前读到的数,把较小的写入 c.txt。这就是"归并"操作。)

9. 编程题 — CSV 解析器

CSV(Comma-Separated Values)是一种用逗号分隔字段的数据格式。文件 data.csv 内容如下:

姓名,年龄,分数
小明,13,96
小红,14,88
小刚,13,92

写程序读取这个文件,跳过第一行(表头),计算并输出所有学生的平均年龄和平均分数(保留 2 位小数)。

(提示:使用 getline(fin, line) 逐行读取,然后解析每行中用逗号分隔的三个字段。可以用 stringfindsubstr 来拆分,或者用 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方式即可。。★核心(课堂必做) ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0114https://hydro.ac/d/srqc/p/J0114struct+sort+文件I/O
◆ 拓展复用J0106同样的题,改成freopen版本
◆ 拓展复用J0103歌唱比赛,改成文件读写

💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 2 道◆拓展题,覆盖不同变式和细节。


配套练习

共3题。文件操作本质是"把cin/cout换成文件"——用旧题改I/O方式即可。★核心(课堂必做) · ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0114https://hydro.ac/d/srqc/p/J0114struct+sort+文件I/O
◆ 拓展复用J0106同样的题,改成freopen版本
◆ 拓展复用J0103歌唱比赛,改成文件读写

练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 2 道◆拓展题,覆盖不同变式和细节。

自查清单

  • [ ] 我会用 freopen 重定向标准输入输出到文件
  • [ ] 我知道 freopen 的优点(代码不改)和局限(不灵活)
  • [ ] 我会用 ifstream 从文件读取各种类型的数据
  • [ ] 我会用 ofstream 向文件写入数据
  • [ ] 我知道如何检查文件是否成功打开
  • [ ] 我知道覆盖模式和追加模式的区别
  • [ ] 我会用 ios::app 在文件末尾追加内容

🚀 下章预告

恭喜你完成了 M4 模块的学习!从函数封装到参数传递,从作用域到递归,从结构体到指针,再到文件读写——你已经掌握了 C++ 核心编程的全部基本功。在 M5 模块中,我们将进入算法世界的第一步:复杂度分析与基础算法——开始真正像竞赛选手一样思考问题!🎉