81 / 103 · C++11 · 约 12 分钟
类不变量:让非法状态进不了对象
构造函数不仅填成员,还负责建立对象承诺;修改操作必须维护同一承诺。以有容量上限的库存为例,把范围检查放进类型内部,并在失败前保持旧值不变,比要求每个调用者自觉检查更可靠。
The C++ Programming Language
已分段通读第1—44章全部正文(印刷页3—1279),含代码、表格、章末建议,另读各版前言;私有文本L140—59048连续覆盖。随后直接核读126个已定位的原PDF图示、公式及上下文页,补齐对象布局、继承箭头、矩阵、流和并发等视觉内容。full指正文及已识别的实质图示读完,不表示逐页校勘全部1366个PDF页面;索引仅导航抽查,未逐条读。原书是C++11基线,C++20差异单列。
查看版本、实际阅读范围与原文入口 →先写规则,再写成员
一个库存对象的承诺是 0 <= used <= capacity。如果把两个整数公开,调用者可以分别修改它们,任何一次赋值都可能制造非法组合。类的价值不在于把函数搬进大括号,而在于把合法状态和允许的变化集中到一个边界。
构造函数检查初始值;读取接口不改变状态;预订操作先检查剩余量再提交。这样,成功返回的构造和修改都维持同一个不变量。参数检查失败不是“库存为零”,而是没有产生新的合法操作结果,示例通过异常明确报告。
失败前不修改:把一次操作做成事务
预订时不要先加后检查。对有符号整数,先加可能已经溢出,随后检查来不及挽回;对无符号整数,回绕也会掩盖错误。已知不变量成立时,比较 amount > capacity - used 不会计算危险的总和。
只有检查通过才执行加法。对于这个只含整数的操作,异常路径保证旧状态完全不变;这不是所有成员函数自动享有的语言保证,而是我们安排语句顺序得到的接口性质。复杂资源修改也应先准备、后提交。
从教材手写容器迁移到现代代码
全书通过资源句柄、异常与特殊成员解释构造、析构和复制;它展示机制,不意味着业务代码应普遍手写动态数组。这个库存类没有资源所有权,所以无需声明析构或复制函数,默认值语义已经足够。
C++20 中仍适用这些设计原则。assert 用于示例检查,不替代面向外部输入的运行时验证;不能因为发布构建会关闭断言,就让非法参数悄悄进入对象。书中早期印次样例有作者勘误,本站程序不照搬其容器实现。
容易答错的地方
- 先执行 used += amount 再判断上限,可能已发生整数溢出;必须先比较剩余容量。
- 不要用断言处理来自外部的非法参数;断言可以被关闭,而接口承诺不能消失。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <stdexcept>
class Stock {
int capacity_;
int used_;
public:
Stock(int capacity, int used) : capacity_(capacity), used_(used) {
if (capacity < 0 || used < 0 || used > capacity)
throw std::invalid_argument("invalid stock");
}
int used() const noexcept { return used_; }
void reserve(int amount) {
if (amount < 0) throw std::invalid_argument("negative amount");
if (amount > capacity_ - used_) throw std::out_of_range("no capacity");
used_ += amount;
}
};
int main() {
Stock s(10, 3);
s.reserve(7);
assert(s.used() == 10);
bool rejected = false;
try { s.reserve(1); } catch (const std::out_of_range&) { rejected = true; }
assert(rejected && s.used() == 10);
rejected = false;
try { Stock invalid(2, 3); } catch (const std::invalid_argument&) { rejected = true; }
assert(rejected);
Stock copy = s;
copy.reserve(0);
assert(copy.used() == s.used());
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-cpp-programming-language.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
增加 release(amount),保证释放过量失败且旧值不变。
查看参考答案
实现 if (amount < 0) throw std::invalid_argument("negative amount"); if (amount > used_) throw std::out_of_range("too much"); used_ -= amount;。在满额对象上 release(4) 后 used 为 6;再 release(7) 应抛异常且仍为 6。先比较后减法同时保证非负和失败不修改。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。