Lesson 16:文件输入输出

对齐原书 Lesson 16「ファイルの入出力」:掌握 ifstream/ofstream、路径、打开模式、格式化与逐行读取、流状态、资源关闭和往返校验。

学习目标

  • 能解释文件路径、打开模式、文件流、缓冲区与磁盘内容之间的关系
  • 能实现文本文件写入、格式化读取和逐行解析,并检查每次打开与转换状态
  • 能设计往返、缺失文件、权限失败、截断与追加实验,区分 EOF、格式错误和 I/O 错误

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

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

文件 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

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

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

小结

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

练习

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

名词解释

名词解释

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

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

资料与写作方式声明

本章以やさしいC++ 第5版, Lesson 16 ファイルの入出力权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

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

讨论

评论区加载中…