写入文件
ofstream out("tasks.txt");
创建或覆盖文件,使用前检查 out 是否有效。追加日志可使用 ios::app。
Easy explanation
程序运行时的 vector 像写在白板上的内容,关机后就被擦掉。文件则像笔记本,程序退出前写入,下次启动再逐行读取。要让两次运行互相理解,必须约定稳定格式:字段顺序、分隔符、特殊字符和版本。
真实文件可能不存在、没有权限、只写了一半或被人手工改坏。可靠程序不会直接崩溃,而会报告哪一行有问题,跳过或停止加载,并尽可能保护原有数据。测试就是提前制造这些情况,确认程序的反应符合设计。
Core concepts
ofstream out("tasks.txt");
创建或覆盖文件,使用前检查 out 是否有效。追加日志可使用 ios::app。
ifstream in("tasks.txt");
逐行 getline 最容易定位错误。文件不存在时要区分首次运行与真正故障。
id|done|title
把对象字段按约定转成一行文本。字段本身含分隔符时需要转义或改用更完善格式。
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";
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;
}
Algorithm thinking
加载流程与用户输入一样:读取、解析、验证、创建对象。不要解析到一半就修改正式容器,可以先把结果放进临时对象,整条记录合法后再加入。若整个文件必须完整才接受,则先加载到临时列表,全部成功后再替换旧数据。
测试从需求推导。每条规则至少有一个正例和一个反例:文件不存在、空文件、只有一行、最后缺换行、字段不足、数字错误、重复编号、标题为空。修复一个错误后把它加入固定测试,防止将来再次出现。
写出示例行、字段类型、分隔符和不允许的字符。
让一行文本转换为临时对象,失败时返回原因。
只有全部字段合法才加入正式容器,避免半条记录。
覆盖正常、空、边界、损坏和权限失败,并保存期望结果。
Common mistakes
打开或写入失败后仍输出“成功”,会让用户以为数据已经保存。
标题含竖线会多出字段,必须禁止、转义或换更可靠格式。
新文件损坏时会同时失去内存中的好数据,应先加载到临时容器。
最危险问题往往来自缺失和损坏文件,必须主动制造失败测试。
先准备一个最小输入,只保留能够重现问题的几行数据;再在关键步骤输出变量值,确认程序究竟在哪一步偏离预期。编译错误从第一条开始处理,运行错误则比较“实际结果”和“期望结果”。修复后别只重跑原来的例子,还要增加空输入、边界值和错误输入,防止问题换一个形式再次出现。
Homework
建议按顺序完成。前四题帮助巩固语法和基本操作,第五到第七题要求把多个知识点组合起来,第八题是小项目。每道题都要先写输入、处理、输出三行计划,再开始敲代码;程序运行后至少测试正常情况、边界情况和错误情况。
把多行日记追加到文件,启动后读取全部内容,处理文件首次不存在。
从文件读取整数,跳过坏行,输出总数、平均值和错误行号。
保存与加载学号、姓名、分数,验证重复学号和 0 到 100 范围。
使用 key=value 保存用户名、语言和难度,忽略未知键并提供默认值。
为任务行解析函数编写至少十个断言,覆盖缺字段、空标题和错误数字。
先写临时文件,确认成功后替换正式文件,模拟中途写入失败。
程序遇到损坏记录时把时间、文件名、行号和原因追加到 error.log。
完成增删改查、启动加载、退出保存、自动编号和损坏文件恢复。
本课验收标准:保存和加载都检查文件状态;解析坏记录不会崩溃或产生半对象;至少有十个自动或可重复测试;关闭程序重新启动后数据保持一致。
提交内容应包括源代码、三组测试输入与输出、一个曾经出现的错误及修复方法。最后请用自己的话解释本课最重要的概念;如果只能照着代码念,还需要再独立重写一次核心例子。