C++ 首批课程:值语义、RAII与库存汇总项目 · 完整库存记录汇总CLI至练习与自检
本轮仅全文静态复核,示例未执行;下文Windows/Linux运行记录属于此前批次,不证明本轮修订源码已经运行。
这条路径从C++自身开始,不把C课程设为强制前置。先用string、vector和资源对象表达业务,再按需要深入底层;理解值/引用、所有权和资源生存期比记住一组手动new/delete写法更早。前置仅为会保存文件、切换终端目录与运行命令。
完整库存记录汇总CLI
路径examples/cpp/inventory-summary。教学格式每行name,quantity,没有引号/转义,不是通用CSV。name为1–32个ASCII字母、数字、下划线或短横线且首字符为字母;quantity为0–100000的无符号十进制,不接受空白、正负号或尾随字符。单行最多256字节、10000条、1000个名称,每个名称累计最多1000000。接受LF/CRLF,空文件输出为空且成功;空行失败。
inventory.hpp
#ifndef INVENTORY_HPP
#define INVENTORY_HPP
#include <istream>
#include <map>
#include <string>
#include <stdexcept>
using Inventory = std::map<std::string, unsigned long>;
Inventory read_inventory(std::istream& input);
#endifinventory.cpp
#include "inventory.hpp"
#include <algorithm>
#include <charconv>
#include <string_view>
static bool valid_name(std::string_view name)
{
auto letter = [](char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); };
return !name.empty() && name.size() <= 32 && letter(name.front()) &&
std::all_of(name.begin(), name.end(), [&](char c) {
return letter(c) || (c >= '0' && c <= '9') || c == '_' || c == '-';
});
}
Inventory read_inventory(std::istream& input)
{
Inventory result;
std::string line;
std::size_t row = 0;
while (std::getline(input, line)) {
++row;
if (!line.empty() && line.back() == '\r') line.pop_back();
auto fail = [&] { throw std::invalid_argument("row " + std::to_string(row) + ": invalid inventory record"); };
if (line.size() > 256 || row > 10000) fail();
const auto comma = line.find(',');
if (comma == std::string::npos || line.find(',', comma + 1) != std::string::npos) fail();
std::string_view name(line.data(), comma);
std::string_view digits(line.data() + comma + 1, line.size() - comma - 1);
unsigned long quantity = 0;
auto [end, error] = std::from_chars(digits.data(), digits.data() + digits.size(), quantity);
if (!valid_name(name) || digits.empty() || error != std::errc{} || end != digits.data() + digits.size() || quantity > 100000) fail();
std::string key(name);
if (!result.contains(key) && result.size() >= 1000) fail();
auto& total = result[key];
if (quantity > 1000000 - total) fail();
total += quantity;
}
if (input.bad() || (input.fail() && !input.eof())) throw std::runtime_error("input read failure");
return result;
}main.cpp
#include "inventory.hpp"
#include <fstream>
#include <iostream>
int main(int argc, char** argv)
{
if (argc != 2) { std::cerr << "usage: inventory-summary FILE\n"; return 2; }
try {
std::ifstream input(argv[1], std::ios::binary);
if (!input) throw std::runtime_error("cannot open file");
const auto inventory = read_inventory(input);
for (const auto& [name, quantity] : inventory) std::cout << name << '=' << quantity << '\n';
std::cout.flush();
if (!std::cout) throw std::runtime_error("output failure");
return 0;
} catch (const std::exception& error) {
std::cerr << "error: " << error.what() << '\n';
return 1;
}
}test_inventory.cpp
#include "inventory.hpp"
#include <cassert>
#include <sstream>
#include <iostream>
int main()
{
std::istringstream input("book,2\r\npen,1\nbook,3\n");
auto result = read_inventory(input);
assert(result.at("book") == 5 && result.at("pen") == 1);
std::istringstream empty("");
assert(read_inventory(empty).empty());
std::istringstream unreadable("book,1\n");
unreadable.setstate(std::ios::failbit);
bool read_failed = false;
try { (void)read_inventory(unreadable); }
catch (const std::runtime_error&) { read_failed = true; }
assert(read_failed);
for (const auto* text : {"book,-1\n", "book,1x\n", "book,100001\n", "bad name,1\n", "book,1,2\n", "\n"}) {
std::istringstream bad(text);
bool caught = false;
try { (void)read_inventory(bad); }
catch (const std::invalid_argument&) { caught = true; }
assert(caught);
}
std::ostringstream too_many;
for (int i = 0; i < 11; ++i) too_many << "book,100000\n";
std::istringstream overflow(too_many.str());
bool caught = false;
try { (void)read_inventory(overflow); }
catch (const std::invalid_argument&) { caught = true; }
assert(caught);
std::cout << "inventory tests passed\n";
}sample.txt
book,2
pen,1
book,3目录内编译zig c++ -std=c++20 -Wall -Wextra -Werror inventory.cpp main.cpp -o inventory-summary.exe,Windows执行.\inventory-summary.exe sample.txt;Linux去.exe并执行./inventory-summary sample.txt。预期按名称排序输出:
book=5
pen=1测试编译inventory.cpp与test_inventory.cpp,运行test-inventory,预期inventory tests passed。不加会关闭assert的-DNDEBUG。覆盖重复聚合、空文件、非法字段和累计超限;实际CLI另验证参数、缺文件、坏行、CRLF和失败时stdout为空。先完成解析再输出,避免部分输出被下游误当成完整结果。
ifstream持有文件资源,read_inventory只借用istream,调用者拥有并负责其生存期;库函数返回map值。name的string_view仅在当前line不变时使用,并在下一次getline之前构造拥有键的string。输入逐行读取后检查长度,因此不是对任意超大单行都有固定内存上限的流式协议解析器。
诊断、退出码与边界
成功0写stdout;无参数或多参数2向stderr写usage;缺文件、格式/累计超限或读失败1向stderr写error。相对输入路径基于启动目录。Windows窄字符路径转换受环境影响,本批使用普通文件名,不宣称任意路径编码均验证。
库在循环后拒绝 badbit,以及没有正常EOF的 failbit;预先失败的输入流不能当空文件成功。CLI输出后刷新并检查cout,输出失败转为退出1;已输出的前缀不能回滚。POSIX默认SIGPIPE可能直接终止进程,这里没有修改信号策略,不能把所有管道错误都承诺成退出1。
undefined reference先检查所有.cpp是否参与链接;no matching function先核对参数类型和const;unique_ptr复制报错应检查所有权是否本来需要转移,不用shared_ptr消灭所有诊断。异常处理不会修复悬空引用等未定义行为,必须先遵守生存期规则。高级内存检查、并发、模板元编程另批验证。
练习与自检
- 用pen,0和重复book输入预测结果。2. 增加按数量降序显示,数量相同时按名称,解释为何不能只换unordered_map。3. 指出项目里拥有资源和借用资源的变量。4. 比较按值返回string与返回局部string_view的安全性。
提示与参考解法
0是合法数量,新名称仍会得到0条目;book重复累加。排序显示可把map条目复制到vector<pair<string,unsigned long>>,用sort比较数量再比较名称,不改变解析约定。ifstream拥有文件,istream&借用它;line/string/map拥有字符和节点,string_view借用当前line。按值返回string拥有结果,可使用返回值优化/移动;指向局部string的view离开函数后悬空,不能靠const修复。
依据:C++ Core Guidelines、标准C++库与资源管理FAQ、中文参考。核对2026-10-17;中文资料用于对照,关键规则以标准与实现文档核对。
继续阅读
- 排错
- 开发实践
- 开发实践