C++ / a working model

89 / 103   ·   C++11   ·   约 13 分钟

输入解析:读完整记录,验证后再提交

先记住这句话

从流中读出一个整数,并不证明整条记录合法。把逐行获取、字段提取、范围校验和结果提交分成阶段,让多余字段、负值与坏格式都留下可解释的失败结果,且不污染上一条有效记录。

本篇内容
  1. 一个成功提取不等于一条合法记录
  2. 先读候选值,再检查结束位置
  3. 错误策略应留给知道上下文的层
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读部分正文

Programming: Principles and Practice Using C++

已读第二版全部章节0—27和附录A—E的可用文字、代码、Drill、Review、Exercises、术语/索引;其中第1、11、22—25、27章与Glossary另用作者完整章节PDF阅读。作者章节共294印刷页;第11/22/23章关键缺页、表和图已补读图像。其余公开全书OCR仍有损坏代码与未恢复的图形/表格,特别第12—16章图形与GUI,因此不声称原版图文无缺口通读;习题未运行。

查看版本、实际阅读范围与原文入口 →

一个成功提取不等于一条合法记录

输入流能把文本转换成类型,却不知道业务要求一行包含几个字段。若只读取名称和数量,字符串 alpha 3 extra 的前两个字段仍能成功提取;若只接受非负数量,整数提取也不会替你排除负数。书中由输入错误走向类不变量,关键正是把这些层次分开。

本例规定一行恰有一个不含空白的名称和一个非负整数,允许行尾空白,不接受额外字段。调用者负责取得整行,解析函数只处理这条有限记录。因此坏记录不会把共享输入流停在半个字段上,也不会把下一行误读成当前记录的补充。

先读候选值,再检查结束位置

函数把字段提取到局部变量,任一失败都立即返回 false。随后用 std::ws 消费允许的尾部空白,并检查是否已到记录末尾。这样数值后缀、额外字段与不完整输入都被拒绝,而换行获取与字段语法仍相互独立。

只有全部检查通过才交换名称并写入整数。前面的字符串分配若抛异常,旧记录也没有变化;默认分配器 string 的交换和 int 赋值在这里不会引入新的分配失败。这个简单提交步骤支持明确的失败不修改承诺,不需要在每个错误分支手工恢复旧字段。

错误策略应留给知道上下文的层

解析函数用布尔结果报告语法或范围不合法,调用层可以显示错误、跳过记录或要求重输;资源耗尽等异常仍自然传播,不把所有失败混成零数量。示例逐一比较坏输入之后的完整旧值,不能只检查“返回了 false”。

PPP 第二版使用 C++11 和部分 C++14 技术,图形库是教材对 FLTK 的封装,不是标准图形接口。这里选择标准字符串流,避免依赖教材头文件和图形环境。C++20 中还可以采用其他解析设施,但无论工具如何改变,字段数量、数值范围、输入结束和提交时点仍需由接口明确规定。

容易答错的地方

  • 在循环里先检查 eof 再读取,会漏判或多处理一次;应先检查提取操作的实际结果。
  • 不要逐字段直接修改输出对象;第二字段失败时可能留下看似有效的半条新记录。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <cassert>
#include <sstream>
#include <string>
struct Record { std::string name; int count; };
bool parse_record(const std::string& line, Record& out) {
    std::istringstream input(line);
    std::string name;
    int count = 0;
    if (!(input >> name >> count) || count < 0) return false;
    input >> std::ws;
    if (!input.eof()) return false;
    out.name.swap(name);
    out.count = count;
    return true;
}
int main() {
    Record record{"old", 8};
    assert(parse_record("alpha 3   ", record));
    assert(record.name == "alpha" && record.count == 3);
    for (const std::string line : {"beta -1", "beta 2 extra", "beta 2x", "beta", ""}) {
        assert(!parse_record(line, record));
        assert(record.name == "alpha" && record.count == 3);
    }
    assert(parse_record("zero 0", record));
    assert(record.name == "zero" && record.count == 0);
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-programming-principles-practice.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

把数量限制为0至100,并给出失败保持旧值的检查。

查看参考答案

在局部变量提取后,把范围条件改成 count < 0 || count > 100;提交代码不变。先解析 good 100 并确认成功,再解析 bad 101,要求返回 false 且名称仍为good、数量仍为100;最后解析 good 0 验证另一边界。不要提交后再把超限数量改回100,那会掩盖输入错误。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录