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;现代写法只能明确标为迁移说明。

围绕“怎样证明写出的格式能被同一协议完整读回,并区分正常结束与读取失败?”,本页要求保留“规范化路径、打开模式、文件内容、每步流状态、读写字段数、往返差异和错误消息。”。若故障“打开失败后仍进入读取循环并使用上一轮值”无法在同一输入下制造首个分岔,应拒绝当前解释,而不是追加随机样例。

为什么文件成功打开只是第一项证据

终端输入输出与文件输入输出都使用流模型,但文件增加了路径、权限、打开模式、持久化和外部修改。程序能构造一个流对象,不代表路径正确、写入已落盘或内容格式符合读取契约。

文件 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、字节序、指针值和编译器布局,跨版本不可移植。

稳定二进制格式应逐字段编码,规定字节序、长度和版本,并验证输入大小。需要成熟兼容性时使用经过验证的序列化库或格式。

往返测试验证写入与读取契约一致

先预测写入两条记录后文件的确切文本,再关闭、重新打开、解析到新对象,比较字段。然后分别测试空文件、缺失文件、非法分数、半条记录和追加模式,确认每个失败路径有明确分类。

三步建立文件证据链

分步1 / 3

第一步:固定路径、模式与格式

写出解析后路径、覆盖/追加策略、字段顺序、编码和范围;打开后立即检查流,并限制文件对象作用域。

正式节点与章专属证据

  • 文件:在“Lesson 16:文件输入输出”中核对输入、状态变化、失败模式和可复现证据;第 1 个节点必须能回到“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。
  • 文件输出:在“Lesson 16:文件输入输出”中核对输入、状态变化、失败模式和可复现证据;第 2 个节点必须能回到“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。
  • 文件输入:在“Lesson 16:文件输入输出”中核对输入、状态变化、失败模式和可复现证据;第 3 个节点必须能回到“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。
  • 打开文件:在“Lesson 16:文件输入输出”中核对输入、状态变化、失败模式和可复现证据;第 4 个节点必须能回到“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。
  • 输入输出错误:在“Lesson 16:文件输入输出”中核对输入、状态变化、失败模式和可复现证据;第 5 个节点必须能回到“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。

先用输入合同检查本页正式节点,再在相同初值下逐步比较正常和失败轨迹,最后只启用“打开失败后仍进入读取循环并使用上一轮值”完成反例与复位。三个交互都必须能独立重置,且重置后再次满足“每次读写后检查流状态,成功往返后关键字段与原对象一致。”。

输入与状态合同

Lesson 16:文件输入输出

怎样证明写出的格式能被同一协议完整读回,并区分正常结束与读取失败?

必须先声明

为文件声明输入类型、有效范围、对象生命周期和失败策略。

可复核证据

保存Lesson 16:文件输入输出的原始输入、初值与第一条可检查诊断。

正式节点:文件、文件输出、文件输入、打开文件、输入输出错误

编译与运行轨迹

同一输入下比较正常与失败路径

  1. 01解析并记录路径与模式
  2. 02验证文件成功打开
  3. 03逐项读写并检查流状态
  4. 04往返比较原值与读回值

不变量:每次读写后检查流状态,成功往返后关键字段与原对象一致。

故障定位与复位

一次只破坏一个前提

基线可继续:每次读写后检查流状态,成功往返后关键字段与原对象一致。

小结

  • 文件流把持久字节资源接入流接口,路径、模式、状态与格式都是契约的一部分
  • ofstream 默认可能截断,追加需显式 app;关键替换应考虑临时文件和原子重命名
  • ifstream 的格式化读取会因 EOF、格式或底层错误停止,读取操作本身应作为循环条件
  • 相对路径基于工作目录,格式转换成功后仍要验证字段范围与跨字段不变量
  • 往返测试连接写入与读取,二进制模式不等于可移植对象序列化

练习

  1. 问题 1:判断模式。 默认 ofstream 与 ios::app 对已有文件分别有什么风险和用途?
  1. 问题 2:修正循环。 为什么 while(!input.eof()) 会多处理旧值,正确结构是什么?
  1. 问题 3:设计往返。 两条姓名/分数记录的最小往返测试包含哪些证据?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

文件
文件系统中持久保存字节序列的命名资源。
文件流
把文件字节连接到 C++ 流接口的对象。
文件输出流
用于格式化并写入文件的 std::ofstream。
打开模式
指定读写、追加、截断和二进制行为的标志。
文件输入流
用于从文件读取并解析值的 std::ifstream。
流状态
记录成功、EOF、格式失败与底层错误的状态集合。

资料与写作方式声明

本章以高桥麻奈 2017 年 C++ 入门教材第 5 版权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…