C++ practical course · lesson 19

C++ 文件读写、错误处理与测试

内存中的数据在程序退出后会消失。本课学习保存和加载,同时把“文件不存在、格式损坏、数据越界”当成必须设计的正常情况。

Easy explanation

保存文件像给程序写一本可重新阅读的记事本。

程序运行时的 vector 像写在白板上的内容,关机后就被擦掉。文件则像笔记本,程序退出前写入,下次启动再逐行读取。要让两次运行互相理解,必须约定稳定格式:字段顺序、分隔符、特殊字符和版本。

真实文件可能不存在、没有权限、只写了一半或被人手工改坏。可靠程序不会直接崩溃,而会报告哪一行有问题,跳过或停止加载,并尽可能保护原有数据。测试就是提前制造这些情况,确认程序的反应符合设计。

目标 1使用输入输出文件流读写文本
目标 2设计并解析简单持久化格式
目标 3处理打开失败与格式错误
目标 4编写正常、边界和错误测试
C++ 文件读写、错误处理与测试通俗学习插图
保存把对象变成文本,加载再把文本还原成经过验证的对象。

Core concepts

先把四个核心知识点说清楚。

01

写入文件

ofstream out("tasks.txt");

创建或覆盖文件,使用前检查 out 是否有效。追加日志可使用 ios::app。

02

读取文件

ifstream in("tasks.txt");

逐行 getline 最容易定位错误。文件不存在时要区分首次运行与真正故障。

03

序列化

id|done|title

把对象字段按约定转成一行文本。字段本身含分隔符时需要转义或改用更完善格式。

04

测试断言

assert(actual == expected);

断言表达必须成立的条件,适合小函数测试;用户输入错误不能只靠 assert 处理。

if (!in) { cout << "无法打开文件"; }

打开后立即检查状态,不能假设文件一定存在。

while (getline(in, line))

逐行读取可以记录行号并精确报告损坏位置。

ofstream out(tempPath); rename(...);

重要数据可先写临时文件,成功后替换,降低写一半损坏风险。

assert(parse("3|1|Read").id == 3);

为解析函数准备固定输入,测试不需要手工操作菜单。

Complete examples

两个例子,把知识变成能运行的程序。

Example 1

例子一:保存任务列表

每行保存编号、完成状态和标题。保存函数只负责输出,接收 const 引用,成功返回 true。

标题若允许竖线就会破坏格式,本例在添加任务时禁止竖线,并把这条规则写进错误提示。

一条任务写一行
struct Task { int id; bool done; string title; };

bool saveTasks(const vector<Task>& tasks, const string& path)
{
    ofstream out(path);
    if (!out) return false;
    for (const Task& task : tasks)
        out << task.id << '|' << task.done << '|' << task.title << '\n';
    return static_cast<bool>(out);
}

if (!saveTasks(tasks, "tasks.txt"))
    cerr << "保存失败,请检查文件权限\n";
tasks.txt1|0|完成数学作业2|1|整理书桌

Example 2

例子二:逐行加载并跳过坏记录

先找到两个分隔符,再分别转换编号和状态。任何一步失败都输出行号并跳过,合法记录继续加载。

解析逻辑最好放入独立函数并返回 optional 或成功标志,这样可以用许多短字符串自动测试。

解析字段、记录行号
vector<Task> loadTasks(const string& path)
{
    ifstream in(path);
    vector<Task> tasks;
    string line;
    int lineNumber = 0;
    while (getline(in, line))
    {
        ++lineNumber;
        size_t a = line.find('|');
        size_t b = line.find('|', a == string::npos ? 0 : a + 1);
        if (a == string::npos || b == string::npos)
        {
            cerr << "跳过第 " << lineNumber << " 行\n";
            continue;
        }
        try {
            tasks.push_back({stoi(line.substr(0, a)),
                line.substr(a + 1, b - a - 1) == "1",
                line.substr(b + 1)});
        } catch (...) { cerr << "数字格式错误\n"; }
    }
    return tasks;
}
成功加载 2 条任务跳过第 3 行:字段不足跳过第 5 行:数字格式错误

Algorithm thinking

把文件当作不可信输入,而不是程序内部数据。

加载流程与用户输入一样:读取、解析、验证、创建对象。不要解析到一半就修改正式容器,可以先把结果放进临时对象,整条记录合法后再加入。若整个文件必须完整才接受,则先加载到临时列表,全部成功后再替换旧数据。

测试从需求推导。每条规则至少有一个正例和一个反例:文件不存在、空文件、只有一行、最后缺换行、字段不足、数字错误、重复编号、标题为空。修复一个错误后把它加入固定测试,防止将来再次出现。

  1. 1
    定义文件格式

    写出示例行、字段类型、分隔符和不允许的字符。

  2. 2
    分离解析函数

    让一行文本转换为临时对象,失败时返回原因。

  3. 3
    验证后提交

    只有全部字段合法才加入正式容器,避免半条记录。

  4. 4
    建立测试清单

    覆盖正常、空、边界、损坏和权限失败,并保存期望结果。

Common mistakes

这些错误很常见,学会自己排查。

1

不检查文件状态

打开或写入失败后仍输出“成功”,会让用户以为数据已经保存。

2

分隔符冲突

标题含竖线会多出字段,必须禁止、转义或换更可靠格式。

3

加载前先清空旧数据

新文件损坏时会同时失去内存中的好数据,应先加载到临时容器。

4

只测正常文件

最危险问题往往来自缺失和损坏文件,必须主动制造失败测试。

本课通用调试法

先准备一个最小输入,只保留能够重现问题的几行数据;再在关键步骤输出变量值,确认程序究竟在哪一步偏离预期。编译错误从第一条开始处理,运行错误则比较“实际结果”和“期望结果”。修复后别只重跑原来的例子,还要增加空输入、边界值和错误输入,防止问题换一个形式再次出现。

Homework

第 19 课课后作业:从模仿到独立完成。

建议按顺序完成。前四题帮助巩固语法和基本操作,第五到第七题要求把多个知识点组合起来,第八题是小项目。每道题都要先写输入、处理、输出三行计划,再开始敲代码;程序运行后至少测试正常情况、边界情况和错误情况。

1

文本日记

把多行日记追加到文件,启动后读取全部内容,处理文件首次不存在。

2

数字统计文件

从文件读取整数,跳过坏行,输出总数、平均值和错误行号。

3

学生 CSV

保存与加载学号、姓名、分数,验证重复学号和 0 到 100 范围。

4

设置文件

使用 key=value 保存用户名、语言和难度,忽略未知键并提供默认值。

5

解析测试

为任务行解析函数编写至少十个断言,覆盖缺字段、空标题和错误数字。

6

安全保存

先写临时文件,确认成功后替换正式文件,模拟中途写入失败。

7

错误日志

程序遇到损坏记录时把时间、文件名、行号和原因追加到 error.log。

8

持久化任务项目

完成增删改查、启动加载、退出保存、自动编号和损坏文件恢复。

本课验收标准:保存和加载都检查文件状态;解析坏记录不会崩溃或产生半对象;至少有十个自动或可重复测试;关闭程序重新启动后数据保持一致。

提交内容应包括源代码、三组测试输入与输出、一个曾经出现的错误及修复方法。最后请用自己的话解释本课最重要的概念;如果只能照着代码念,还需要再独立重写一次核心例子。

Lesson 19 summary

持久化不只是写文件,更是定义格式、验证数据和保护旧结果。

ifstreamofstreamserializationerror handlingtesting

下一课把前面所有知识组合成结课项目,从需求、类设计和容器选择一直做到保存、测试与演示。