Skip to content

第8章 文件操作 ​


🎯 本章学习目标 ​

通过本章的学习,你将能够:

  1. 理解文件作用:知道文件用于持久化存储数据,解决内存数据易失的问题
  2. 掌握freopen重定向:熟练使用 freopen 进行输入输出重定向
  3. 掌握文件打开模式:理解 r/w/a 等模式的含义和区别
  4. 使用C++文件流:会用 ifstream 读文件、ofstream 写文件
  5. 使用C风格文件:了解 fopen/fclose/fscanf/fprintf 的用法
  6. 正确判断文件末尾:掌握 while (fin >> x) 的正确写法,避免 eof() 陷阱
  7. 区分覆盖与追加:知道默认写模式是覆盖,ios::app 是追加
  8. 检查打开成功:养成打开文件后检查 is_open() 的好习惯
  9. 正确关闭文件:理解 close() 的作用,知道为什么要关闭文件
  10. 处理路径问题:知道 Windows 路径中反斜杠需要转义

🗺️ 本章知识图谱 ​

文件操作
├── 文件基础
│   ├── 为什么需要文件(持久化)
│   ├── 文本文件 vs 二进制文件
│   └── 文件路径(绝对/相对)
├── freopen 重定向
│   ├── freopen 三参数
│   ├── 重定向 stdin
│   ├── 重定向 stdout
│   └── 竞赛常用写法
├── C++ 文件流
│   ├── ifstream 读文件
│   ├── ofstream 写文件
│   ├── fstream 读写
│   ├── is_open() 检查
│   ├── close() 关闭
│   ├── >> 和 << 操作
│   ├── getline 读一行
│   └── ios::app 追加模式
├── C 风格文件
│   ├── FILE* 指针
│   ├── fopen/fclose
│   ├── fscanf/fprintf
│   ├── fgetc/fputc
│   └── EOF 判断
├── 常见模式
│   ├── "r" 只读
│   ├── "w" 只写(覆盖)
│   ├── "a" 追加
│   ├── "rb"/"wb" 二进制
│   └── ios::in / ios::out / ios::app
├── 易错点
│   ├── 不检查打开成功
│   ├── 忘记关闭文件
│   ├── 覆盖 vs 追加搞混
│   ├── eof() 的错误用法
│   ├── >> 与 getline 混用
│   └── 路径反斜杠转义
└── 典型应用
    ├── 读写整数/字符串
    ├── 文件统计(字符/行数)
    ├── 学生成绩管理
    └── 竞赛数据输入输出

一、为什么需要文件操作? ​

我们之前写的所有程序,数据都存放在变量和数组中,存在三个明显问题:

问题说明
数据易失程序运行结束后内存释放,所有数据消失
输入不便大量测试数据只能逐行从键盘敲入,效率低、易出错
结果无法保存输出只在屏幕上一闪而过,无法后续查看或复用

文件操作恰好能解决这三个痛点:

  1. 持久化存储——数据写入磁盘文件,程序关闭后依然存在
  2. 批量输入——从文件读取大量数据,无需手动输入
  3. 结果保存——将程序输出写入文件,方便查阅和提交

🔥 GESP考点说明:GESP四级主要考查文本文件的读写操作,涵盖C风格文件操作(FILE*)、C++风格文件流(fstream)以及freopen重定向三大方式。二进制文件仅需概念了解。


二、文本文件与二进制文件 ​

在开始写代码之前,先搞清楚文件存储的两种基本形式。

对比项文本文件二进制文件
存储内容字符的 ASCII/UTF-8 编码数据在内存中的原始二进制表示
可读性可用记事本直接打开阅读需要特定程序解析,直接打开是乱码
换行符Windows 下为 \r\n,Linux 下为 \n无换行概念,原样存储字节
数值 123 的存储三个字符 '1' '2' '3'(字节 49 50 51)整数 123 的 4 字节二进制表示
典型扩展名.txt、.csv、.cpp.exe、.jpg、.dat
GESP要求⭐⭐⭐ 重点掌握仅了解概念

⚠️ 易错点:用 fprintf 或 ofstream 写入整数 123 时,文本文件中存的是三个字符 '1'、'2'、'3'(ASCII码分别为 49、50、51),共3个字节;而用 fwrite 以二进制方式写入,存的是整数 123 的 4 字节补码表示。考试常考判断题!


三、文件重定向(freopen) ​

3.1 什么是重定向? ​

默认情况下:

  • cin / scanf 从键盘读取数据(标准输入流 stdin)
  • cout / printf 向屏幕输出数据(标准输出流 stdout)

重定向就是把标准输入/输出"指向"一个文件,之后所有的 cin/cout(或 scanf/printf)操作自动变为文件读写,业务代码无需任何修改。

3.2 freopen 函数原型 ​

cpp
#include <cstdio>  // freopen 所在头文件

FILE* freopen(const char* filename, const char* mode, FILE* stream);
//            ↑                   ↑                 ↑
//         文件名/路径          打开模式          被重定向的流

常用调用方式:

cpp
freopen("input.txt", "r", stdin);   // 标准输入重定向到 input.txt
freopen("output.txt", "w", stdout); // 标准输出重定向到 output.txt

3.3 完整示例 ​

cpp
#include <iostream>
#include <cstdio>  // freopen 头文件
using namespace std;

int main() {
    // 将标准输入重定向到 input.txt
    freopen("input.txt", "r", stdin);
    // 将标准输出重定向到 output.txt
    freopen("output.txt", "w", stdout);

    // 之后的代码与平时完全一样!无需修改 cin/cout
    int a, b;
    cin >> a >> b;
    cout << a + b << endl;

    return 0;
}

假设 input.txt 内容为:

3 5

程序运行后,output.txt 中会写入:

8

3.4 竞赛中的使用技巧 ​

cpp
#include <iostream>
#include <cstdio>
using namespace std;

int main() {
    // 本地调试时使用文件输入输出
    // 提交到OJ时注释掉这两行即可
    freopen("input.txt", "r", stdin);
    freopen("output.txt", "w", stdout);

    int n;
    cin >> n;
    cout << n * n << endl;

    return 0;
}

💡 实用技巧:很多OJ(包括GESP机考环境)会自动将标准输入/输出绑定到测试数据文件,保留 freopen 语句通常不影响评测。但更稳妥的做法是用条件编译,提交时无需修改代码:

cpp
#ifdef LOCAL
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
#endif

编译时加上 -DLOCAL 选项即可启用本地文件重定向。

⚠️ 易错点:freopen 的第二个参数是C风格字符串(双引号),如 "r"、"w";而后面要学的 fstream 的打开模式是常量,如 ios::in、ios::out。两者不要混淆,这是选择题常见陷阱!


四、C风格文件操作 ​

C风格文件操作围绕 FILE*(文件指针)展开,定义在 <cstdio> 头文件中。虽然C++提供了更面向对象的文件流,但C风格函数在竞赛和底层编程中仍然常见,GESP四级也要求掌握。

4.1 文件打开与关闭:fopen / fclose ​

cpp
#include <cstdio>

FILE* fopen(const char* filename, const char* mode);
// 成功返回文件指针,失败返回 NULL

int fclose(FILE* stream);
// 关闭文件,成功返回0,失败返回 EOF

文件打开模式速查表:

模式含义文件不存在时文件已存在时
"r"只读(文本)打开失败,返回 NULL从头读取
"w"只写(文本)创建新文件清空原有内容
"a"追加(文本)创建新文件在末尾追加,保留原内容
"r+"读写(文本)打开失败从头读写,不清空
"w+"读写(文本)创建新文件清空原有内容
"a+"读+追加(文本)创建新文件末尾追加,可读原内容
"rb"只读(二进制)同 "r"同 "r"
"wb"只写(二进制)同 "w"同 "w"
"ab"追加(二进制)同 "a"同 "a"

🔥 高频考点:"w" 模式打开已有文件会清空原内容!"a" 模式则保留原内容在末尾追加。考试常考"以下哪种模式不会覆盖文件原有内容"——答案是 "a" 或 "r"。

打开与关闭的完整模板:

cpp
#include <cstdio>

int main() {
    // 以只读模式打开文件
    FILE* fp = fopen("data.txt", "r");

    // 必须检查是否打开成功!
    if (fp == NULL) {
        printf("文件打开失败!\n");
        return 1;
    }

    // ... 文件读写操作 ...

    fclose(fp);  // 用完必须关闭
    return 0;
}

4.2 格式化读写:fscanf / fprintf ​

这对函数的用法和 scanf/printf 几乎一样,只是多了第一个参数——文件指针。

cpp
int fscanf(FILE* stream, const char* format, ...);
int fprintf(FILE* stream, const char* format, ...);

完整示例:

cpp
#include <cstdio>

int main() {
    // === 写入数据 ===
    FILE* fout = fopen("scores.txt", "w");
    if (fout == NULL) {
        printf("无法创建文件!\n");
        return 1;
    }

    // fprintf 用法与 printf 完全一致,只是输出到文件
    fprintf(fout, "张三 85\n");
    fprintf(fout, "李四 92\n");
    fprintf(fout, "王五 78\n");
    fclose(fout);

    // === 读取数据 ===
    FILE* fin = fopen("scores.txt", "r");
    if (fin == NULL) {
        printf("无法打开文件!\n");
        return 1;
    }

    char name[20];
    int score;
    // fscanf 用法与 scanf 一致,只是从文件读取
    while (fscanf(fin, "%s %d", name, &score) == 2) {
        printf("%s: %d\n", name, score);
    }

    fclose(fin);
    return 0;
}

输出:

张三: 85
李四: 92
王五: 78

⚠️ 易错点:fscanf 的返回值是成功读取的数据项数,不是读取的字符数。当到达文件末尾时返回 EOF(通常为 -1)。上面用 == 2 判断是否成功读取了两个数据项(姓名和成绩),这是安全读取到文件末尾的推荐写法。

4.3 字符读写:fgetc / fputc ​

逐个字符读写文件,适合处理不确定格式或需要逐字符分析的场景。

cpp
int fgetc(FILE* stream);   // 读取一个字符,返回ASCII码;到末尾返回 EOF
int fputc(int ch, FILE* stream); // 写入一个字符,成功返回该字符,失败返回 EOF

完整示例:文件复制

cpp
#include <cstdio>

int main() {
    FILE* fin  = fopen("source.txt", "r");
    FILE* fout = fopen("copy.txt", "w");

    if (fin == NULL || fout == NULL) {
        printf("文件打开失败!\n");
        return 1;
    }

    int ch;  // 注意用 int,不是 char!因为要容纳 EOF(-1)
    while ((ch = fgetc(fin)) != EOF) {
        fputc(ch, fout);  // 逐字符复制
    }

    fclose(fin);
    fclose(fout);
    printf("文件复制完成!\n");
    return 0;
}

⚠️ 易错点:fgetc 的返回值类型是 int 而不是 char!因为它需要返回 EOF(值为 -1),而 char 可能是无符号类型(0~255),无法表示 -1。如果写成 char ch; while ((ch = fgetc(fp)) != EOF),当读取到字节值为 255(0xFF)时会被误判为 EOF!

4.4 字符串读写:fgets / fputs ​

cpp
char* fgets(char* str, int num, FILE* stream);
// 最多读取 num-1 个字符,遇到换行符或EOF停止,自动加 '\0'

int fputs(const char* str, FILE* stream);
// 写入字符串到文件(不自动加换行)

完整示例:逐行读取并添加行号

cpp
#include <cstdio>
#include <cstring>

int main() {
    FILE* fin = fopen("article.txt", "r");
    if (fin == NULL) {
        printf("无法打开文件!\n");
        return 1;
    }

    char line[256];
    int lineNum = 0;
    while (fgets(line, sizeof(line), fin) != NULL) {
        lineNum++;
        // fgets 会把换行符也读进来,可以去掉
        int len = strlen(line);
        if (len > 0 && line[len - 1] == '\n') {
            line[len - 1] = '\0';
        }
        printf("%3d: %s\n", lineNum, line);
    }

    fclose(fin);
    return 0;
}

⚠️ fgets 与 gets 的区别:fgets 要求指定最大读取长度 num,更安全(防止缓冲区溢出);已被废弃的 gets 不限制长度,存在安全隐患。考试中可能出现"fgets的第二个参数含义"的选择题。

4.5 feof 判断文件末尾 ​

cpp
int feof(FILE* stream);
// 到达文件末尾返回非0值,否则返回0

feof 的工作机制与C++流的 eof() 类似——它在一次读取操作因到达末尾而失败后才返回真值。

cpp
// ❌ 不推荐的写法:可能导致最后一个数据被处理两次
int x;
while (!feof(fp)) {
    fscanf(fp, "%d", &x);
    printf("%d ", x);  // 最后一个数可能被输出两次
}

// ✅ 推荐的写法:直接判断 fscanf 的返回值
int x;
while (fscanf(fp, "%d", &x) == 1) {
    printf("%d ", x);
}

// ✅ 逐字符读取时判断 EOF
int ch;
while ((ch = fgetc(fp)) != EOF) {
    putchar(ch);
}

🔥 高频考点:为什么不推荐 while (!feof(fp))?因为 feof() 只有在读取操作失败后才被置位。循环条件为真时,可能已经到达末尾但尚未尝试读取,导致循环体多执行一次。记住:用读取函数的返回值判断,而不是 feof!


五、C++风格文件流 ​

C++ 在 <fstream> 头文件中提供了三个文件流类,使用 >> 和 << 运算符进行读写,语法与 cin/cout 高度一致。

5.1 三个核心类 ​

类名全称功能头文件
ifstreaminput file stream读文件(输入)<fstream>
ofstreamoutput file stream写文件(输出)<fstream>
fstreamfile stream读写文件<fstream>

💡 记忆技巧:i 代表 input(输入/读取),o 代表 output(输出/写入)。和 cin(输入)、cout(输出)的方向一致。

5.2 ofstream:写文件 ​

cpp
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    // 方式一:构造函数直接打开文件(推荐,更简洁)
    ofstream fout("result.txt");

    // 方式二:先创建对象,再调用 open()
    // ofstream fout;
    // fout.open("result.txt");

    // 检查文件是否成功打开(必做!)
    if (!fout.is_open()) {
        cout << "文件打开失败!" << endl;
        return 1;
    }

    // 使用 << 运算符写入,和 cout 用法完全一样
    fout << "Hello, File!" << endl;
    fout << 100 << " " << 200 << endl;
    fout << "平均分:" << 95.5 << endl;

    // 关闭文件(必做!)
    fout.close();
    return 0;
}

result.txt 内容:

Hello, File!
100 200
平均分:95.5

5.3 ifstream:读文件 ​

cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

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

    // 方式一:使用 >> 运算符读取(自动跳过空白字符)
    int a, b;
    fin >> a >> b;
    cout << a << " + " << b << " = " << a + b << endl;

    // 方式二:使用 getline 逐行读取
    fin.ignore();  // 跳过 >> 残留的换行符
    string line;
    while (getline(fin, line)) {
        cout << "读到一行:" << line << endl;
    }

    fin.close();
    return 0;
}

⚠️ 易错点:>> 运算符会自动跳过空格、换行等空白字符,但读取后换行符会留在输入缓冲区。如果紧接着使用 getline,会读到一个空行。解决方法是在 getline 前调用 fin.ignore() 跳过残留换行符。

5.4 fstream:同时读写 ​

fstream 可以同时进行读和写操作,需要指定 ios::in | ios::out 模式。

cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main() {
    // 以读写模式打开文件
    fstream file("data.txt", ios::in | ios::out);

    if (!file.is_open()) {
        cout << "文件打开失败!" << endl;
        return 1;
    }

    // 写入数据
    file << "第一行数据" << endl;
    file << "第二行数据" << endl;

    // 写入后文件指针在末尾,需要移回开头才能读取
    file.seekg(0, ios::beg);  // seekg = seek get,移动读指针

    // 读取数据
    string line;
    while (getline(file, line)) {
        cout << line << endl;
    }

    file.close();
    return 0;
}

💡 了解即可:fstream 同时读写需要移动文件指针(seekg/seekp),操作较复杂,GESP四级考试中很少涉及,重点掌握 ofstream 和 ifstream 即可。

5.5 文件打开模式标志 ​

C++ 文件流通过 ios 命名空间中的常量指定打开模式:

模式标志含义说明
ios::in读模式打开文件用于读取(ifstream 默认)
ios::out写模式表示"可写入",单独使用时不一定清空文件;ofstream 默认会加上 ios::trunc
ios::app追加模式写入内容追加到文件末尾,不覆盖原内容
ios::trunc覆盖模式打开时清空文件原有内容(ofstream 默认行为)
ios::ate定位末尾打开后文件指针定位到末尾
ios::binary二进制模式以二进制方式打开文件

模式可以组合使用,用 |(按位或)连接:

cpp
// 以读写模式打开
fstream file("data.txt", ios::in | ios::out);

// 以追加模式写入(保留原内容)
ofstream fout("log.txt", ios::app);

// 以二进制模式读取
ifstream fin("image.dat", ios::in | ios::binary);

🔥 高频考点:ios::app(追加)与 ios::trunc(覆盖)的区别是常考判断题!

  • ofstream 默认使用 ios::out | ios::trunc,即覆盖写入——打开时清空原内容!
  • 如果不想覆盖,必须显式指定 ios::app。

5.6 eof() 判断与安全读取 ​

与C风格的 feof 类似,C++流的 eof() 也有"滞后性"问题。

cpp
// ❌ 不推荐:可能导致最后一个数据重复处理
int x;
while (!fin.eof()) {
    fin >> x;
    cout << x << " ";  // 最后一个值可能输出两次
}

// ✅ 推荐:直接将读取表达式作为循环条件
int x;
while (fin >> x) {
    cout << x << " ";
}

// ✅ 逐字符读取
char ch;
while (fin.get(ch)) {
    cout << ch;
}

🔥 高频考点:为什么不推荐 while (!fin.eof())?

eof() 在一次读取操作因到达文件末尾而失败后才返回 true。假设文件中只有一个数字 42:

  1. 第1次循环:!fin.eof() 为 true,fin >> x 成功读到 42
  2. 第2次循环:!fin.eof() 仍为 true(尚未尝试过末尾读取),fin >> x 失败,x 保持为 42
  3. eof() 终于变为 true,循环结束

结果 42 被输出了两次!永远使用 while (fin >> x) 的写法。


六、C风格 vs C++风格 对比 ​

对比项C风格(FILE*)C++风格(fstream)
头文件<cstdio><fstream>
核心对象FILE* 文件指针ifstream/ofstream/fstream 对象
打开文件fopen("f.txt", "r")ifstream fin("f.txt")
关闭文件fclose(fp)fin.close()
格式化读fscanf(fp, "%d", &x)fin >> x
格式化写fprintf(fp, "%d", x)fout << x
字符读fgetc(fp)fin.get(ch)
字符写fputc(ch, fp)fout.put(ch)
判断末尾fscanf 返回值 / fgetc 返回 EOFfin >> x 返回值 / fin.get(ch)
检查打开成功fp == NULL!fin.is_open()
打开模式字符串 "r"/"w"/"a"常量 ios::in/ios::out/ios::app
竞赛适用场景与 freopen 配合,底层操作面向对象,多文件操作更方便

💡 备考建议:两种风格都要求掌握。选择题常考语法辨析(如参数类型、返回值判断),编程题推荐使用更简洁的 freopen 或 fstream。


七、文件操作常见易错点 ​

易错点1:不检查文件是否打开成功 ​

cpp
// ❌ 错误:文件不存在时后续操作全部失败,变量为随机值
ifstream fin("nonexistent.txt");
int x;
fin >> x;          // 读取失败
cout << x;         // 输出未初始化的垃圾值

// ✅ 正确:打开后立即检查
ifstream fin("nonexistent.txt");
if (!fin.is_open()) {
    cout << "文件打开失败!" << endl;
    return 1;
}

🔥 文件打开失败的常见原因:文件不存在(读模式)、路径错误、权限不足、文件被其他程序占用。

易错点2:忘记关闭文件 ​

cpp
// ❌ 错误:写入的数据可能还在缓冲区,未真正写入磁盘
ofstream fout("data.txt");
fout << "重要数据";
// 没有 close(),程序异常退出时数据可能丢失

// ✅ 正确
ofstream fout("data.txt");
fout << "重要数据";
fout.close();  // 刷新缓冲区,确保数据落盘

⚠️ 为什么必须关闭文件?

  1. 写入的数据可能还留在缓冲区中,close() 会强制刷新,确保数据写入磁盘
  2. 操作系统对同时打开的文件数量有限制
  3. 显式关闭是最佳实践,考试中忘记 close() 可能被扣分

易错点3:覆盖 vs 追加 ​

cpp
// ofstream 默认覆盖模式,会清空原有内容!
ofstream fout1("log.txt");          // 等价 ios::out | ios::trunc
fout1 << "新内容";                   // 原有内容全部丢失!

// 追加模式:保留原有内容,新内容写在末尾
ofstream fout2("log.txt", ios::app);
fout2 << "追加的内容";

易错点4:>> 与 getline 混用 ​

cpp
// ❌ 问题代码
int n;
string line;
fin >> n;            // 读取整数后,换行符留在缓冲区
getline(fin, line);  // 读到残留换行符,line 为空字符串!

// ✅ 解决方案:中间加 ignore()
int n;
string line;
fin >> n;
fin.ignore();        // 跳过残留的换行符
getline(fin, line);

易错点5:Windows 路径中的反斜杠 ​

cpp
// ❌ 错误:单反斜杠是转义字符
ifstream fin("C:\Users\test\data.txt");  // 编译错误或路径错误!

// ✅ 正确:双反斜杠转义,或直接用正斜杠
ifstream fin1("C:\\Users\\test\\data.txt");
ifstream fin2("C:/Users/test/data.txt");  // C++ 也能识别正斜杠

易错点速查表 ​

易错点后果正确做法
不检查 is_open() / NULL打开失败时产生未定义行为打开后立即检查
不调用 close() / fclose()数据可能未写入磁盘用完后立即关闭
用 while(!eof()) / while(!feof())最后一个数据可能重复读取用读取函数的返回值判断
>> 后直接 getline读到空行中间加 ignore()
默认构造覆盖文件原有数据丢失追加用 ios::app 或 "a"
Windows 路径用单反斜杠编译错误或路径错误用 \\ 或 /
fgetc 返回值用 char 接收字节 0xFF 被误判为 EOF用 int 接收

八、综合实战:学生成绩管理 ​

下面通过一个综合示例,串联文件读写的完整流程。

cpp
#include <iostream>
#include <fstream>
#include <string>
#include <iomanip>
using namespace std;

int main() {
    // ========== 第一步:写入成绩数据 ==========
    ofstream fout("scores.txt");
    if (!fout.is_open()) {
        cout << "无法创建文件!" << endl;
        return 1;
    }

    // 写入5名学生的姓名和成绩
    fout << "张三 85" << endl;
    fout << "李四 92" << endl;
    fout << "王五 78" << endl;
    fout << "赵六 95" << endl;
    fout << "钱七 88" << endl;
    fout.close();

    // ========== 第二步:读取并统计 ==========
    ifstream fin("scores.txt");
    if (!fin.is_open()) {
        cout << "无法打开文件!" << endl;
        return 1;
    }

    string name;
    int score;
    int sum = 0, count = 0;
    int maxScore = -1, minScore = 101;
    string maxName, minName;

    // 使用 while (fin >> ...) 安全读取到文件末尾
    while (fin >> name >> score) {
        cout << name << ": " << score << endl;
        sum += score;
        count++;
        if (score > maxScore) {
            maxScore = score;
            maxName = name;
        }
        if (score < minScore) {
            minScore = score;
            minName = name;
        }
    }
    fin.close();

    // ========== 第三步:输出统计结果 ==========
    cout << "----------" << endl;
    cout << fixed << setprecision(1);
    cout << "平均分:" << (double)sum / count << endl;
    cout << "最高分:" << maxName << " " << maxScore << endl;
    cout << "最低分:" << minName << " " << minScore << endl;

    // ========== 第四步:将统计结果追加写入文件 ==========
    ofstream foutApp("scores.txt", ios::app);  // 追加模式
    if (!foutApp.is_open()) {
        cout << "无法打开文件进行追加!" << endl;
        return 1;
    }
    foutApp << "----------" << endl;
    foutApp << "平均分:" << fixed << setprecision(1)
            << (double)sum / count << endl;
    foutApp << "最高分:" << maxName << " " << maxScore << endl;
    foutApp << "最低分:" << minName << " " << minScore << endl;
    foutApp.close();

    return 0;
}

屏幕输出:

张三: 85
李四: 92
王五: 78
赵六: 95
钱七: 88
----------
平均分:87.6
最高分:赵六 95
最低分:王五 78

💡 代码要点:① 写入用默认模式(覆盖),追加统计结果用 ios::app;② 读取用 while (fin >> name >> score) 安全判断末尾;③ 每次打开文件后都检查 is_open();④ 用完立即 close()。


九、📝 实战练习 ​

练习题 1(编程题) ​

题目: 编写程序,将 1 到 N(N 从键盘输入)的所有偶数写入文件 even.txt,每行一个偶数;然后再从该文件中读取所有偶数,计算它们的和并输出到屏幕。

🔑 点击查看答案与解析

参考代码:

cpp
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    int n;
    cin >> n;

    // 第一步:写入偶数
    ofstream fout("even.txt");
    if (!fout.is_open()) {
        cout << "文件创建失败!" << endl;
        return 1;
    }
    for (int i = 2; i <= n; i += 2) {
        fout << i << endl;
    }
    fout.close();

    // 第二步:读取并求和
    ifstream fin("even.txt");
    if (!fin.is_open()) {
        cout << "文件打开失败!" << endl;
        return 1;
    }
    int num, sum = 0;
    while (fin >> num) {
        sum += num;
    }
    fin.close();

    cout << "偶数之和:" << sum << endl;
    return 0;
}

输入:

10

输出:

偶数之和:30

解析:

  1. 第一步用 ofstream 创建/覆盖文件,将 2, 4, 6, 8, 10 逐行写入
  2. 第二步用 ifstream 读取,while (fin >> num) 安全判断文件末尾
  3. 求和:2 + 4 + 6 + 8 + 10 = 30
  4. 注意每次文件操作完毕后都调用 close(),先关闭写流再打开读流

练习题 2(编程题) ​

题目: 文件 input.txt 中存放了 n 个学生的姓名和成绩(第一行为 n,后续 n 行每行一个姓名和一个整数成绩)。要求使用 freopen 重定向,读取数据后计算平均分,并将成绩高于平均分的学生姓名和成绩输出到文件 output.txt。

input.txt 内容:

5
张三 85
李四 92
王五 78
赵六 95
钱七 88
🔑 点击查看答案与解析

参考代码:

cpp
#include <iostream>
#include <cstdio>
#include <string>
using namespace std;

int main() {
    // 文件重定向:之后的 cin 从 input.txt 读,cout 写到 output.txt
    freopen("input.txt", "r", stdin);
    freopen("output.txt", "w", stdout);

    int n;
    cin >> n;

    string names[1005];
    int scores[1005];
    int sum = 0;

    for (int i = 0; i < n; i++) {
        cin >> names[i] >> scores[i];
        sum += scores[i];
    }

    double avg = (double)sum / n;
    cout << "平均分:" << avg << endl;
    cout << "高于平均分的学生:" << endl;
    for (int i = 0; i < n; i++) {
        if (scores[i] > avg) {
            cout << names[i] << " " << scores[i] << endl;
        }
    }

    return 0;
}

output.txt 内容:

平均分:87.6
高于平均分的学生:
李四 92
赵六 95
钱七 88

解析:

  1. 使用 freopen 将 stdin 重定向到 input.txt,stdout 重定向到 output.txt
  2. 代码主体与普通键盘输入/屏幕输出完全一样,无需修改 cin/cout
  3. 平均分使用 (double)sum / n 进行浮点数除法,避免整数除法截断
  4. 高于 87.6 分的有李四(92)、赵六(95)、钱七(88)

⚠️ 注意:使用 freopen 时,数组大小要足够(这里开 1005),防止数据量较大时越界。


练习题 3(编程题) ​

题目: 使用C风格文件操作(fopen/fgetc/fclose),统计文件 article.txt 中大写字母、小写字母、数字字符和其他字符的个数,并将统计结果输出到屏幕。

🔑 点击查看答案与解析

参考代码:

cpp
#include <cstdio>
#include <cctype>  // isupper, islower, isdigit

int main() {
    FILE* fp = fopen("article.txt", "r");
    if (fp == NULL) {
        printf("文件打开失败!\n");
        return 1;
    }

    int upper = 0, lower = 0, digit = 0, other = 0;
    int ch;  // 必须用 int,因为要容纳 EOF(-1)

    while ((ch = fgetc(fp)) != EOF) {
        if (isupper(ch))      upper++;
        else if (islower(ch)) lower++;
        else if (isdigit(ch)) digit++;
        else                  other++;
    }

    fclose(fp);

    printf("大写字母:%d\n", upper);
    printf("小写字母:%d\n", lower);
    printf("数字字符:%d\n", digit);
    printf("其他字符:%d\n", other);
    return 0;
}

假设 article.txt 内容为:

Hello 2024!
GESP C++ Level-4.

输出:

大写字母:7
小写字母:8
数字字符:5
其他字符:10

解析:

  1. 使用 fopen("article.txt", "r") 以只读文本模式打开文件
  2. 检查 fp == NULL 判断是否打开成功
  3. fgetc 逐个字符读取,返回值用 int 类型接收(容纳 EOF)
  4. 使用 <cctype> 中的 isupper/islower/isdigit 宏分类统计
  5. 统计完毕后 fclose(fp) 关闭文件

⚠️ 易错点提醒:本题故意使用C风格操作,考试中如果题目明确要求 FILE* 或 fopen,就不能用 ifstream。注意 fgetc 返回值必须用 int!


练习题 4(单选题) ​

题目: 以下关于文件打开模式的说法,错误的是( ) A. "r" 模式打开文件用于读取,文件必须存在
B. "w" 模式打开文件用于写入,文件不存在则创建,存在则覆盖
C. "a" 模式打开文件用于追加,新内容写在文件末尾
D. "r+" 模式打开文件用于读写,文件不存在时自动创建

🔑 点击查看答案与解析

答案: D

解析:

  • A 正确 ✅:"r" = read,只读模式,文件必须存在,否则打开失败
  • B 正确 ✅:"w" = write,只写模式,会清空原有内容(覆盖)
  • C 正确 ✅:"a" = append,追加模式,新内容写在末尾
  • D 错误 ❌:"r+" 是读写模式,但文件必须存在,不存在时不会自动创建("w+" 才会创建)

💡 记忆口诀:r 是读(必须存在),w 是写(清空重来),a 是追加(末尾添加)。带 + 号就是读写双模式。


练习题 5(判断题) ​

题目: 使用 freopen("data.txt", "r", stdin); 后,cin 会从文件 data.txt 读取数据,而不是从键盘读取。( )

🔑 点击查看答案与解析

答案: 正确 ✅

解析: freopen 的作用是重定向标准流。freopen("data.txt", "r", stdin) 把标准输入流 stdin 重定向到文件,之后所有从 stdin 读取的操作(包括 cin、scanf、getchar 等)都会从文件读取。

🍎 生活比喻:就像把自来水管从"市政供水"切换到"自家水箱",打开水龙头流出来的就是水箱里的水了。竞赛中常用 freopen 避免手动输入大量测试数据。


练习题 6(单选题) ​

题目: 以下哪种方式是读取文件的正确写法?( ) A. while (fin.eof()) { fin >> x; ... }
B. while (!fin.eof()) { fin >> x; ... }
C. while (fin >> x) { ... }
D. for (int i = 0; i < 100; i++) { fin >> x; ... }

🔑 点击查看答案与解析

答案: C

解析:

  • A 错误:eof() 为 true 时表示已经到末尾了,应该是 !eof()
  • B 错误:!eof() 的写法有陷阱——读完最后一个数据后,eof() 还没变成 true,会多读一次无效数据
  • C 正确 ✅:fin >> x 的返回值就是 fin 对象,它可以被转换为 bool 类型。读取成功为 true,失败(到末尾)为 false。这是最安全的写法
  • D 错误:硬编码循环次数容易读多或读少

🔥 高频考点:考试中常考 eof() 的错误用法。记住:永远用 while (fin >> x) 的写法,不要用 eof() 判断!


练习题 7(多选题) ​

题目: 以下说法正确的有( )

① 文件操作完成后应该调用 close() 关闭文件
② ofstream 默认是追加模式,不会覆盖原有内容
③ ifstream 打开不存在的文件时,is_open() 返回 false
④ fgetc 的返回值应该用 char 类型接收
⑤ freopen 的第三个参数是要重定向的流(stdin/stdout/stderr) A. ①②③
B. ①③⑤
C. ②③④
D. ②④⑤

🔑 点击查看答案与解析

答案: B

解析:

  • ① 正确 ✅:关闭文件可以刷新缓冲区,确保数据写入磁盘
  • ② 错误 ❌:ofstream 默认是覆盖模式(ios::trunc),不是追加
  • ③ 正确 ✅:文件不存在时打开失败,is_open() 返回 false
  • ④ 错误 ❌:fgetc 返回值应该用 int 类型接收,因为要容纳 EOF(值为 -1,超出 char 范围)
  • ⑤ 正确 ✅:freopen 的第三个参数是流,常用 stdin/stdout

选 B(①③⑤)。


练习题 8(编程题) ​

题目: 文件 nums.txt 中存储了若干个整数(用空格分隔)。请编写程序:

  1. 从文件中读取所有整数
  2. 找出其中的最大值和最小值
  3. 将结果写入文件 result.txt,格式:最大值=xx,最小值=xx
🔑 点击查看答案与解析

参考答案:

cpp
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    ifstream fin("nums.txt");
    if (!fin.is_open()) {
        cout << "无法打开输入文件!" << endl;
        return 1;
    }

    int x;
    int maxVal, minVal;
    bool first = true;

    while (fin >> x) {
        if (first) {
            maxVal = minVal = x;
            first = false;
        } else {
            if (x > maxVal) maxVal = x;
            if (x < minVal) minVal = x;
        }
    }
    fin.close();

    ofstream fout("result.txt");
    if (!fout.is_open()) {
        cout << "无法创建输出文件!" << endl;
        return 1;
    }
    fout << "最大值=" << maxVal << endl;
    fout << "最小值=" << minVal << endl;
    fout.close();

    return 0;
}

解析:

这是文件操作的经典题型,核心步骤:

  1. 用 ifstream 打开输入文件,检查是否成功
  2. 用 while (fin >> x) 循环读取所有整数
  3. 遍历过程中记录最大值和最小值
  4. 用 ofstream 打开输出文件,写入结果
  5. 用完文件后及时 close()

💡 小技巧:第一个数既是最大值也是最小值,后面每个数都和当前最大/最小值比较更新。


十、📊 考点总结表 ​

考点重要程度出题形式易错点
文件打开模式(r/w/a)⭐⭐⭐⭐选择/判断各种模式的区别记混
freopen 重定向⭐⭐⭐⭐编程题/代码填空三参数的顺序和含义
ifstream 读文件⭐⭐⭐⭐编程题/代码阅读忘记检查 is_open()
ofstream 写文件⭐⭐⭐⭐编程题默认覆盖,不是追加
close() 关闭文件⭐⭐⭐⭐选择/判断忘记关闭导致数据丢失
eof() 的正确用法⭐⭐⭐⭐⭐选择/代码阅读用 eof() 判断会多读一次
覆盖 vs 追加⭐⭐⭐⭐选择/判断默认是覆盖(trunc),追加需ios::app
is_open() 检查⭐⭐⭐⭐⭐选择/判断/编程打开失败仍继续操作
C风格 fopen/fclose⭐⭐⭐选择/代码阅读NULL 判断、fgetc用int接收
>> 与 getline 混用⭐⭐⭐选择/代码阅读残留换行符导致读空行

⭐ 必背速记卡 ​

┌─────────────────────────────────────────────┐
│          文件操作 · 必背速记卡               │
├─────────────────────────────────────────────┤
│  1. 文件用于持久化存储,程序结束数据不丢失   │
│  2. freopen三参数:文件名、模式、流          │
│  3. "r"只读(必须存在),"w"只写(覆盖),"a"追加│
│  4. 二进制加b:"rb"读二进制,"wb"写二进制    │
│  5. ifstream 读文件,ofstream 写文件         │
│  6. 打开后检查 is_open(),失败要处理         │
│  7. 用完文件 close(),刷新缓冲区保数据       │
│  8. 读文件用 while (fin >> x),别用 eof()   │
│  9. eof() 陷阱:读完最后一个还没到eof状态    │
│ 10. ofstream 默认覆盖,追加用 ios::app      │
│ 11. C风格:fopen打开,fclose关闭,FILE*指针  │
│ 12. fgetc 返回值用 int 接收,容纳 EOF(-1)   │
│ 13. >> 和 getline 混用:中间加 ignore()     │
│ 14. Windows路径反斜杠要转义:\\ 才表示一个\  │
│ 15. 竞赛推荐 freopen:最简单,代码改动最小   │
└─────────────────────────────────────────────┘

百炼成钢,融会贯通