跳至内容
Topos Uranos

摘要

本课结束课程的第七单元,即关于文本与文件的单元。它用所学的内容重写导言中的程序,那个程序会悄无声息地丢失数据,而且量错含有非 ASCII 字符的名字;它重建从字符串通向文件格式的概念链;它把其中适用于任何语言的内容与 C++ 特有的内容区分开;它汇集本单元的未定义行为,包括检测器能拦下的和谁也看不到的;它在结业测评中重复入门测评的技能;最后以六道综合整个单元的挑战题收尾。

学习目标

  1. 结合编码处理真实的文本:预测字符串的字节数,统计其中的字符,并避免破坏 UTF-8 的操作。
  2. 稳健地读取输入和文件,区分无效数据与数据结束,并把每个数据作为完整的文本来校验。
  3. 按规定的格式生成结果文件,并报告每一个无效行及其行号。
  4. 识别本单元的未定义行为(悬垂视图、越出字符串的下标),并把它与同样悄然发生的已定义错误区分开。
  5. 在本单元所学内容中,区分适用于任何语言的部分与 C++ 特有的部分。

本单元的收获

本单元的目标是教会把文本当作数据来处理,并让程序在读写文件时不因格式错误的输入而崩溃。这一目标已经达成,所依靠的只是导言声明为已知的内容:第 2 单元的“字符即编码”,第 3 单元的位,第 6 单元的向量和记录,以及第 5 单元的带契约的函数。成绩单程序原来用 >> 读取,丢失了欧阳 娜娜和大伟,还宣称“小明”比“Lily”长;现在它这样写:读取文件的各行,按最后一个空格分出成绩,用函数 parseInt(出自正确读取输入一课)校验它,通过错误流报告无效行,按字符度量名字,并把名单留在另一个文件中。为了让示例自成一体,程序先写出数据文件,而在实际中这个文件本来就已存在。

C++ 程序
#include <charconv>#include <cstddef>#include <fstream>#include <iostream>#include <print>#include <string>#include <string_view>​// 判断一段文本是否恰好是一个整数。// 前置条件:无。// 后置条件:同“正确读取输入”一课。bool parseInt(std::string_view text, int& value){    int result{};    const auto last{text.data() + text.size()};    const auto [stop, error]{std::from_chars(text.data(), last, result)};    if (error != std::errc{} || stop != last) {        return false;    }    value = result;    return true;}​// UTF-8 文本的字符数。// 前置条件:text 是有效的 UTF-8。// 后置条件:返回 text 的字符数(不是字节数)。std::size_t letterCount(std::string_view text){    std::size_t count{0};    for (const char c : text) {        if ((c & 0xC0) != 0x80) {            ++count;        }    }    return count;}​int main(){    {        std::ofstream out{"grades.txt"};        std::println(out, "Lily 65\n小明 70\n欧阳 娜娜 58\n大伟 六\n小红 61");    }    std::ifstream in{"grades.txt"};    std::ofstream report{"report.txt"};    if (!in || !report) {        std::println(std::cerr, "无法打开这些文件");        return 1;    }    std::string line{};    std::string longest{};    int lineNumber{0};    while (std::getline(in, line)) {        ++lineNumber;        const std::size_t space{line.rfind(' ')};        int grade{};        if (space == std::string::npos || !parseInt(std::string_view{line}.substr(space + 1), grade)) {            std::println(std::cerr, "第 {} 行无效:“{}”", lineNumber, line);            continue;        }        const std::string name{line.substr(0, space)};        std::println(report, "{:<10}{:>3}", name, grade);        if (letterCount(name) > letterCount(longest)) {            longest = name;        }    }    std::println("最长的名字:{}({} 个字符)", longest, letterCount(longest));}
输出
第 4 行无效:“大伟 六”最长的名字:欧阳 娜娜(5 个字符)

屏幕上只显示错误信息和结论;名单在文件里,运行程序之后,可以用命令 cat(在 PowerShell 中用 Get-Content)显示它:

终端
$ cat report.txtLily       65小明       70欧阳 娜娜  58小红       61

每一部分都对应导言中的一个缺陷。按行读取和 rfind 允许名字中含有空格;npos 和 parseInt 拒绝大伟那一行,报告它的行号,并让小红继续;letterCount 统计字符,于是“欧阳 娜娜”以五个字符、十三个字节名正言顺地胜出;而 report.txt 在 main 结束、其流关闭时就完整了。

字符串

字符串一课把 std::string 介绍为带有文本操作的 char 序列。它教会了用 find 查找,它返回一个位置或 npos,并且总要显式地比较这个值;用 substr 提取,它要求位置有效;它介绍了 std::string_view,作为参数是安全的,但若比它所观察的文本活得更久就很危险;作为对照,还介绍了 C 风格字符串。

文本不只是 ASCII

文本不只是 ASCII一课区分了字符、它的码位和它的字节,并解释了 UTF-8 如何把位分配到一到四个字节中。由此推出:size 统计的是字节;字符用 (c & 0xC0) != 0x80 来统计;逐字节地反转、转成大写或截断会破坏文本,而查找和拼接不会;以及标准库不做哪些事。

正确读取输入

正确读取输入一课把输入当作不可信的边界。它教会了用 std::getline 读取行,用 fail() 和 eof() 区分无效数据、放不下的数和数据结束,用 clear 和 ignore 恢复,用 std::from_chars 把数作为文本来校验,并要求整个文本都被消耗;最后以 parseInt 和 readInt 收尾,它们是带契约的边界函数。

文本文件

文本文件一课表明,文件就是流,具有与 std::cin 相同的状态。它把检查打开是否成功定为规则;证明了为什么 while (!in.eof()) 会多处理一个数据;教会了用 std::println(out, …) 写入,覆盖或追加,并用 std::cerr 把提示与结果分开;最后把一切归结为一种规定好并经过校验的行格式。