Lesson 16:文件输入输出
以路径、打开模式、格式、流状态和往返测试建立文件输入输出合同。
学习目标
- 能解释文件路径、打开模式、文件流、缓冲区与磁盘内容之间的关系
- 能实现文本文件写入、格式化读取和逐行解析,并检查每次打开与转换状态
- 能设计往返、缺失文件、权限失败、截断与追加实验,区分 EOF、格式错误和 I/O 错误
来源、版次与标准边界
“Lesson 16:文件输入输出”以 SB Creative 出版社书页核定高桥麻奈著、第 5 版、2017 年 6 月 14 日发行、ISBN 978-4-7973-9259-3;出版社页面标 596 页。CiNii Books 书目与 16 课目录记录正文 xxiii+571 页,日本国会图书馆书目记录 571 页。页数口径存在前置页差异,因此本课程不拿页码充当目录证据。
“Lesson 16:文件输入输出”的中文说明、示例、交互、练习和答案均为独立教学重写。公开资料只确认 16 个 Lesson 标题;本页列出的细分概念是课程教学映射,不冒充原书逐级小节。技术规则参考 ISO C++ 标准入口复核,但 2017 年教材的 Visual Studio 2017 语境不会被静默升级为 C++23;现代写法只能明确标为迁移说明。
围绕“怎样证明写出的格式能被同一协议完整读回,并区分正常结束与读取失败?”,本页要求保留“规范化路径、打开模式、文件内容、每步流状态、读写字段数、往返差异和错误消息。”。若故障“打开失败后仍进入读取循环并使用上一轮值”无法在同一输入下制造首个分岔,应拒绝当前解释,而不是追加随机样例。
为什么文件成功打开只是第一项证据
终端输入输出与文件输入输出都使用流模型,但文件增加了路径、权限、打开模式、持久化和外部修改。程序能构造一个流对象,不代表路径正确、写入已落盘或内容格式符合读取契约。
↡文件系统中可持久保存字节序列的命名资源;程序通过路径和打开模式访问。 ↡把文件字节序列接入 C++ 流接口的对象,输入流读取,输出流写入。文件 I/O 的证据链至少包含:解析后的路径、打开结果、每次格式化操作状态、关闭/刷新结果、重新读取后的内容。只看“程序没报错”无法证明数据完整。
ofstream 把值格式化为文件字符
std::ofstream 用于输出文件。构造时传路径会尝试打开;成功后 << 与 cout 类似,把值格式化为字符写入流缓冲区。
#include <fstream>
std::ofstream output{"scores.txt"};
if (!output) {
std::cerr << "cannot open scores.txt for writing\n";
return 1;
}
output << "Mina 92\n";
output << "Kai 85\n";流对象离开作用域会关闭文件,关闭前缓冲数据会尝试写出。对关键数据,还应在写后检查 output 状态;磁盘满、设备错误等可能在打开成功后才发生。
打开模式决定保留还是覆盖旧内容
输出流默认通常创建文件并截断旧内容。std::ios::app 让每次写入追加到末尾,std::ios::binary 禁止某些平台文本转换,std::ios::in/out 指明读写方向。
std::ofstream log{"events.log", std::ios::app};
if (!log) {
return 1;
}
log << "application started\n";追加适合日志,不适合需要原子替换的配置。更新关键文件时,常见策略是写临时文件、确认成功后再重命名,避免进程中断留下半份内容。
ifstream 从文件字符解析目标类型
std::ifstream 用于读取。格式化提取 >> 跳过前导空白,并按目标类型解析;读取失败可能是正常 EOF、格式不匹配或底层 I/O 错误。
std::ifstream input{"scores.txt"};
if (!input) {
std::cerr << "cannot open scores.txt for reading\n";
return 1;
}
std::string name;
int score = 0;
while (input >> name >> score) {
std::cout << name << ": " << score << '\n';
}循环把“完整读取一条 name/score 记录”作为继续条件。若文件末尾正常结束,提取失败使循环停止;若中间出现 Kai unknown,也会停止,但原因是格式错误。退出后应检查 input.eof() 与 input.bad(),并记录当前记录号。
getline 读取整行,解析器处理字段
名字可能含空格时,>> name 只读一个词。可以先用 std::getline 取得原始行,再用 std::istringstream 或明确分隔规则解析。
#include <sstream>
std::string line;
int lineNumber = 0;
while (std::getline(input, line)) {
++lineNumber;
std::istringstream record{line};
std::string name;
int score = 0;
if (!(record >> name >> score)) {
std::cerr << "invalid record at line " << lineNumber << '\n';
continue;
}
}保留原始行与行号能给出可定位错误。若格式允许空格、转义和引号,应使用 CSV/JSON 等成熟解析器,而不是用空格切分冒充完整格式。
流状态是一组可查询事实
流维护 good、eof、fail、bad 等状态,用于区分正常结束、格式失败和输入输出错误。fail 表示上一次格式化或操作失败,bad 表示更严重的底层错误,eof 表示尝试读取时到达文件末尾。它们可能同时成立。
不要写 while (!input.eof()) 再读取:EOF 只有在一次读取尝试越过末尾后才设置,这种结构常多处理一次旧值。应把读取操作本身放进条件:while (input >> value) 或 while (getline(input,line))。
相对路径取决于进程工作目录
"scores.txt" 不是相对源文件,而是相对程序运行时当前工作目录。IDE、测试工具和终端可能设置不同目录,导致“本机能用、换入口找不到”。
#include <filesystem>
std::cout << std::filesystem::current_path() << '\n';诊断路径问题时输出解析后的绝对路径。生产程序应从命令行、配置或平台目录 API 获得路径,不要假设可执行文件旁边总可写。
文本格式需要版本与边界规则
把两列写成 name score 看似简单,但名字含空格、分隔符、换行或非法编码时如何处理?文件契约应规定字段顺序、分隔/转义、数值范围、字符编码和版本。
解析后仍要验证业务约束,例如 score 在 0 到 100;成功转成 int 只证明语法像整数。未知字段是拒绝、忽略还是保留,也应由兼容策略决定。
写入完成不等于永久落盘
flush 把 C++ 流缓冲提交给操作系统,close 结束文件句柄,但操作系统和硬件仍可能缓存。普通练习无需手工调用系统同步;关键事务则要使用数据库、原子替换和平台持久化协议。
显式 output.close() 后可以检查状态,适合在同一作用域立即重新打开文件。更常见的做法是缩小流对象作用域,让 RAII 自动关闭,再进入读取阶段。
二进制文件不是直接 dump 任意对象
二进制模式只改变流的文本转换行为,不会自动定义对象序列化格式。直接写类对象内存会携带 padding、字节序、指针值和编译器布局,跨版本不可移植。
稳定二进制格式应逐字段编码,规定字节序、长度和版本,并验证输入大小。需要成熟兼容性时使用经过验证的序列化库或格式。
往返测试验证写入与读取契约一致
先预测写入两条记录后文件的确切文本,再关闭、重新打开、解析到新对象,比较字段。然后分别测试空文件、缺失文件、非法分数、半条记录和追加模式,确认每个失败路径有明确分类。
三步建立文件证据链
第一步:固定路径、模式与格式
写出解析后路径、覆盖/追加策略、字段顺序、编码和范围;打开后立即检查流,并限制文件对象作用域。
正式节点与章专属证据
- 文件:在“Lesson 16:文件输入输出”中核对输入、状态变化、失败模式和可复现证据;第 1 个节点必须能回到“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。
- 文件输出:在“Lesson 16:文件输入输出”中核对输入、状态变化、失败模式和可复现证据;第 2 个节点必须能回到“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。
- 文件输入:在“Lesson 16:文件输入输出”中核对输入、状态变化、失败模式和可复现证据;第 3 个节点必须能回到“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。
- 打开文件:在“Lesson 16:文件输入输出”中核对输入、状态变化、失败模式和可复现证据;第 4 个节点必须能回到“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。
- 输入输出错误:在“Lesson 16:文件输入输出”中核对输入、状态变化、失败模式和可复现证据;第 5 个节点必须能回到“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。
先用输入合同检查本页正式节点,再在相同初值下逐步比较正常和失败轨迹,最后只启用“打开失败后仍进入读取循环并使用上一轮值”完成反例与复位。三个交互都必须能独立重置,且重置后再次满足“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。
输入与状态合同
Lesson 16:文件输入输出
怎样证明写出的格式能被同一协议完整读回,并区分正常结束与读取失败?
必须先声明
为文件声明输入类型、有效范围、对象生命周期和失败策略。
可复核证据
保存Lesson 16:文件输入输出的原始输入、初值与第一条可检查诊断。
正式节点:文件、文件输出、文件输入、打开文件、输入输出错误
编译与运行轨迹
同一输入下比较正常与失败路径
- 01解析并记录路径与模式
- 02验证文件成功打开
- 03逐项读写并检查流状态
- 04往返比较原值与读回值
不变量:每次读写后检查流状态,成功往返后关键字段与原对象一致。
故障定位与复位
一次只破坏一个前提
小结
- 文件流把持久字节资源接入流接口,路径、模式、状态与格式都是契约的一部分
- ofstream 默认可能截断,追加需显式 app;关键替换应考虑临时文件和原子重命名
- ifstream 的格式化读取会因 EOF、格式或底层错误停止,读取操作本身应作为循环条件
- 相对路径基于工作目录,格式转换成功后仍要验证字段范围与跨字段不变量
- 往返测试连接写入与读取,二进制模式不等于可移植对象序列化
练习
- 问题 1:判断模式。 默认 ofstream 与 ios::app 对已有文件分别有什么风险和用途?
- 问题 2:修正循环。 为什么
while(!input.eof())会多处理旧值,正确结构是什么?
- 问题 3:设计往返。 两条姓名/分数记录的最小往返测试包含哪些证据?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 文件
- 文件系统中持久保存字节序列的命名资源。
- 文件流
- 把文件字节连接到 C++ 流接口的对象。
- 文件输出流
- 用于格式化并写入文件的 std::ofstream。
- 打开模式
- 指定读写、追加、截断和二进制行为的标志。
- 文件输入流
- 用于从文件读取并解析值的 std::ifstream。
- 流状态
- 记录成功、EOF、格式失败与底层错误的状态集合。