跳至内容
Topos Uranos

摘要

程序计算出的一切都会在它结束时消失,除非它把结果写在某个比它活得更久的地方。本课介绍这个地方,即文本文件,以及让我们无须学习任何根本上新的东西就能使用它的思想:文件像流一样读写,所用的操作与键盘和屏幕相同。我们将学习 std::ifstream 和 std::ofstream,在使用文件之前检查它是否已经打开,以及决定在哪里查找文件的工作文件夹;正确的读取循环 while (std::getline(in, line)),对比使用 eof() 的循环,后者会多处理一个数据,并讲清多出这个数据的确切原因;用 std::println 写入、追加模式以及离开代码块时的关闭。本课最后讨论行格式:用逗号分隔字段,用 std::from_chars 校验字段,并在规格说明中决定如何处理格式错误的行。

学习目标

  1. 打开一个文件用于读或写,并在使用之前检查它是否已经打开,区分不存在的文件和在另一个文件夹中查找的文件。
  2. 用 while (std::getline(in, line)) 逐行读取文件,并通过跟踪流的状态解释为什么 while (!in.eof()) 会多处理一个数据。
  3. 用 std::println(out, …) 把结果写入文件,并在替换其内容与在末尾追加之间作出选择。
  4. 把一个逗号分隔值文件处理成一个记录向量,校验每一行,并报告无效行及其行号。

文件就是流

持久性

程序的变量存在于内存中,而程序的内存在程序结束时被释放:下一次运行开始时什么也不记得。要让一个数据比产生它的程序活得更久,它必须留在一种持久的介质上,而通用的介质就是文件:一个有名字、保存在磁盘上的字节序列,操作系统在多次运行之间保留它。文本文件是其字节为字符的文件,字符采用 UTF-8 编码(见文本不只是 ASCII一课),并组织成由字符 '\n' 分隔的行。这是最朴素也最持久的格式:任何编辑器都能打开它,任何语言都能读取它,人也能一眼看过去检查它。

然而,C++ 并不把文件当作与键盘不同的东西。从输入、输出与错误一课起,std::cin 就是一个输入流:一个按顺序消耗的字符来源,带有一个记录上一次操作是否失败的状态。为读取而打开的文件是另一个输入流,类型为 std::ifstream(来自 input file stream);为写入而打开的文件是一个输出流,类型为 std::ofstream。两者都声明在头文件 <fstream> 中。由此可知,在正确读取输入一课中学到的关于 std::cin 的一切,即运算符 >>、std::getline 和错误状态,原封不动地适用于文件。

打开并检查

文件在创建流时打开,文件名写在花括号中:std::ifstream in{"grades.txt"};。打开可能失败(文件不存在、没有读取权限、文件名写错),而失败既不会让程序停止,也不会产生任何消息:它只是让流处于错误状态。处于错误状态的流用作条件时值为 false;因此,检查就是 if (!in),而且必须在第一次读取之前进行。下面的程序试图打开一个不存在的文件。

C++ 程序
#include <fstream>#include <iostream>#include <print>​int main(){    std::ifstream in{"grades.txt"};    if (!in) {        std::println(std::cerr, "无法打开 grades.txt");        return 1;    }    int first{};    in >> first;    std::println("第一个成绩:{}", first);}
输出
无法打开 grades.txt

程序以代码 1 结束。

有三个细节值得注意。第一个是 std::cerr(来自 <iostream>),即错误流:它像标准输出一样写到屏幕上,但它是另一个通道,因此当输出被重定向到文件时,错误消息不会与结果混在一起;std::println 接受它应写入的流作为第一个参数。第二个是 return 1;:非零的终止码,如程序的解剖一课所述,告诉运行程序的人出了问题。第三个是如果没有这项检查会发生什么:对处于错误状态的流执行 in >> first 什么也不做,程序会若无其事地写出“第一个成绩:0”。一个看起来像真的假结果比一条错误消息更糟;由此得出本课的规则:每个文件在打开时都要检查。

相对路径与工作文件夹

名字 "grades.txt" 并没有说明文件在哪个文件夹中。这样的名字是一个相对路径,它以工作文件夹为起点来解析:工作文件夹是运行程序时所在的文件夹,不一定是可执行文件所在的文件夹,也不一定是源代码所在的文件夹。下面的程序保存在文件夹 project 中,旁边有一个文件 data.txt,它打开这个文件并写出其第一行;程序运行了两次,第一次在 project 中,第二次在上一级文件夹中。

终端
$ cd project$ g++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -g -fsanitize=address,undefined -fno-sanitize-recover=undefined main.cpp -o main$ ./main第一行:来自文件的问候$ cd ..$ ./project/main无法打开 data.txt

可执行文件是同一个,文件也还在原处;唯一改变的是工作文件夹,以及随之改变的查找 data.txt 的位置。这是“找不到文件”最常见的原因。用一个按钮运行程序的编辑器会自行选择工作文件夹(在 Visual Studio Code 中通常是编辑器中打开的文件夹,不过这取决于任务的配置),因此在弄清编辑器的做法之前,最好在终端中、在数据所在的文件夹里运行程序。相反的诱惑是在代码中写一个绝对路径,例如 "/home/ana/curso/data.txt" 或 "C:\\Users\\Ana\\data.txt",这在一台机器上解决了问题,却在其他所有机器上制造了问题:只要换一个人运行,或者文件夹被移动,程序就不再工作。推荐的做法是在已知工作文件夹的前提下使用相对路径,更好的做法是把文件名作为由用户提供的数据。