Chapter 15:Friends, Exceptions, and More
对齐第6版 Chapter 15:掌握 friend class/member、nested class、exception 与栈展开、RTTI、dynamic_cast 和类型转换运算符。
学习目标
- 能比较 friend function、friend class、friend member 与 nested class 的名字、对象和访问边界
- 能实现类型化 exception 的 throw/try/catch,解释 stack unwinding 与 RAII 清理,并复现未匹配和重抛路径
- 能判断 RTTI 是否必要,比较
dynamic_cast、typeid与其他 type cast operators 的证明责任
机制总览
Chapter 15:Friends, Exceptions, and More:机制路径
- 1
为什么访问授权、错误传播和类型探测都需要窄边界
friend、exception 与 RTTI 看似是三个附加主题,实际都在跨越普通局部边界:friend 跨 private 访问,throw 跨调用帧返回, dynamic cast 跨静态类型查看动态类型。越界能力越强,越要限制授予对象、传播范围和失败语义。
- 2
friend class 适合紧密但明确的协作
友元类(friend class)让被指定类的所有成员访问授权类的 private/protected 数据。友元关系不对称、不传递、不继承:A 把 B 设为 friend,不代表 A 能访问 B,也不代表 B 的派生类自动获得权限。
- 3
friend member 把权限缩到一个成员函数
友元成员(friend member)只授权另一个类的指定成员。它减少意外依赖,但需要安排前置声明、类定义和成员定义顺序,让双方类型在使用点完整或已声明。
章级决策实验
Chapter 15:Friends, Exceptions, and More:机制与证据
切换《Chapter 15:Friends, Exceptions, and More》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么访问授权、错误传播和类型探测都需要窄边界
friend、exception 与 RTTI 看似是三个附加主题,实际都在跨越普通局部边界:friend 跨 private 访问,throw 跨调用帧返回, dynamic cast 跨静态类型查看动态类型。越界能力越强,越要限制授予对象、传播范围和失败语义。
可核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「为什么访问授权、错误传播和类型探测都需要窄边界」的状态变化。
学完《Chapter 15:Friends, Exceptions, and More》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 15:Friends, Exceptions, and More:失效与核验
为什么访问授权、错误传播和类型探测都需要窄边界
典型失效
若只复述「为什么访问授权、错误传播和类型探测都需要窄边界」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。
核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「为什么访问授权、错误传播和类型探测都需要窄边界」的状态变化。
friend class 适合紧密但明确的协作
典型失效
若只复述「friend class 适合紧密但明确的协作」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。
核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「friend class 适合紧密但明确的协作」的状态变化。
friend member 把权限缩到一个成员函数
典型失效
若只复述「friend member 把权限缩到一个成员函数」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。
核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「friend member 把权限缩到一个成员函数」的状态变化。
为什么访问授权、错误传播和类型探测都需要窄边界
friend、exception 与 RTTI 看似是三个附加主题,实际都在跨越普通局部边界:friend 跨 private 访问,throw 跨调用帧返回,dynamic_cast 跨静态类型查看动态类型。越界能力越强,越要限制授予对象、传播范围和失败语义。
friend class 适合紧密但明确的协作
友元类(friend class)让被指定类的所有成员访问授权类的 private/protected 数据。友元关系不对称、不传递、不继承:A 把 B 设为 friend,不代表 A 能访问 B,也不代表 B 的派生类自动获得权限。
↡由一个类明确授权其全部成员访问自身 private/protected 成员的另一个具名类。class Storage;
class Inspector {
public:
bool verify(const Storage&) const;
};
class Storage {
friend class Inspector;
public:
explicit Storage(int checksum) : checksum_{checksum} {}
private:
int checksum_;
};若 Inspector 只有 verify() 需要访问,授权整个类过宽。friend declaration 还涉及声明顺序:编译器必须先知道 Inspector/其成员的声明,才能精确授予。
friend member 把权限缩到一个成员函数
友元成员(friend member)只授权另一个类的指定成员。它减少意外依赖,但需要安排前置声明、类定义和成员定义顺序,让双方类型在使用点完整或已声明。
↡被另一个类单独授予 private/protected 访问权的某一个具名成员函数。class Storage;
class Inspector {
public:
bool verify(const Storage&) const;
};
class Storage {
friend bool Inspector::verify(const Storage&) const;
private:
int checksum_{0};
};更窄的授权仍不能替代领域接口设计。若多类不断需要读 checksum,应考虑稳定 public observation 或重新划分 owner,而不是继续添加 friend。
scope · one named nonmembergets · private/protected access
not · membership or transitive friendship
scope · all members of named classgets · private/protected access
not · reverse friendship
scope · one member of another classgets · narrow collaboration
not · other members' access
scope · name inside enclosing class scopegets · member-like access rules
not · automatic outer object
nested class 隐藏辅助类型名字,不自动绑定外层对象
嵌套类(nested class)声明在另一个类的 scope 中,适合隐藏 Node 等实现类型。它是独立类型,不像某些语言的 inner object 那样自动携带 enclosing object 的 this;要访问某个外层实例仍需显式 pointer/reference。
class Queue {
private:
struct Node {
int value;
Node* next;
};
Node* front_{nullptr};
};外部调用者不需要知道 Node,Queue 实现却能用完整类型表达链表。嵌套只改变名字与访问,不改变 Node 对象的分配、复制或销毁责任。
exception 把错误结果沿调用链传播
异常(exception)适合当前函数无法按正常契约完成、且错误需要跨多个调用层传播的情况。throw 创建异常对象并寻找匹配 handler;try 标出受保护语句;catch 按类型处理。领域错误类型应携带诊断所需上下文,而不是只抛字符串。
#include <stdexcept>
#include <string>
class ParseError : public std::runtime_error {
public:
ParseError(std::size_t line, std::string message)
: std::runtime_error{std::move(message)}, line_{line} {}
std::size_t line() const noexcept { return line_; }
private:
std::size_t line_;
};
int parseCount(std::string_view token, std::size_t line) {
if (token.empty()) throw ParseError{line, "missing count"};
// validate and convert
return 0;
}异常类型按 public inheritance 形成分类;先 catch 具体派生,后 catch 基类,否则宽 handler 会遮蔽窄 handler。通常以 const& 捕获,避免切片与复制。
stack unwinding 依赖 RAII 完成清理
抛出后,运行时逐帧查找 handler,并销毁离开 scope 的已完成 automatic objects,这叫栈展开(stack unwinding)。析构函数必须可靠,因此资源应在取得后立即交给 RAII owner。
↡异常寻找匹配 handler 时逐层退出调用帧,并逆序销毁已构造自动对象的过程。void load(const std::filesystem::path& path) {
std::ifstream input{path};
if (!input) throw std::runtime_error{"cannot open input"};
std::vector<Record> records;
parse(input, records); // if this throws, input/vector clean themselves
}若用裸 new、文件句柄或锁且尚未交给对象,栈展开不会猜测如何释放。RAII 让正常 return 和 throw 共用析构路径。
frame · parse() localsaction · create exception objectlifetime · destroy completed automatic objectsframe · load() localsaction · no matching catchlifetime · RAII members release in reverse orderframe · run() catchaction · bind const referencelifetime · exception object remains aliveframe · application policyaction · translate/log/rethrowlifetime · invariants restored before continuehandler 是策略边界,不是每层都要捕获
能修复、翻译语义、补充上下文或记录后终止的一层才 catch。无策略的中间层应让异常继续传播。裸 throw; 保留当前异常动态类型重抛;throw e; 可能复制/切片。
try {
load(configPath);
} catch (const ParseError& error) {
std::cerr << "line " << error.line() << ": " << error.what() << '\n';
throw; // preserve dynamic exception object
} catch (const std::exception& error) {
std::cerr << "load failed: " << error.what() << '\n';
}没有匹配 handler 时程序调用 std::terminate;违反 noexcept 也会 terminate。内存分配失败通常抛 std::bad_alloc,不要以空指针检查普通 throwing new。
RTTI 在多态层次中检查动态类型
运行时类型识别(runtime type identification, RTTI)主要由 dynamic_cast 和 typeid 提供。对 pointer 的失败 dynamic_cast 返回 nullptr;对 reference 的失败转换抛 std::bad_cast;源基类通常必须是 polymorphic(至少一个 virtual member)。
void inspect(Shape& shape) {
if (auto* circle = dynamic_cast<Circle*>(&shape)) {
std::cout << circle->radius();
} else {
shape.draw(std::cout);
}
}频繁测试每个派生类型再分支,通常应把行为移入 virtual function。RTTI 适合插件边界、可选能力或无法修改基类接口的兼容层,而不是取代多态。
先预测 pointer/reference 成功和失败、非多态源四种结果,再切换实验面板核对;同时记录是否存在更好的 virtual protocol。
type cast operators 对应不同证明责任
C++ type cast operators 不应互换使用:dynamic_cast 验证多态关系;static_cast 执行编译期允许的显式转换但不检查向下转换动态类型;const_cast 只改变 cv 限定,写入原本 const 对象仍未定义;reinterpret_cast 重新解释低层表示,要求严格平台/别名/对齐证据。
Shape* base = getShape();
Circle* checked = dynamic_cast<Circle*>(base);
double ratio = static_cast<double>(count) / total;C-style cast 会尝试多类转换并隐藏意图,审计困难。每次 cast 都应回答源/目标类型、可能损失、运行时检查、对齐/生命周期和失败处理。
三步审计跨边界能力
第一步:画最窄访问授权
列出 friend function/class/member 与 nested helper 实际需要的数据,验证非对称、不传递和声明顺序。
scope · one named nonmembergets · private/protected access
not · membership or transitive friendship
scope · all members of named classgets · private/protected access
not · reverse friendship
scope · one member of another classgets · narrow collaboration
not · other members' access
scope · name inside enclosing class scopegets · member-like access rules
not · automatic outer object
小结
- friend classes and members 是具名、单向、非传递授权;nested class 不自动绑定外层对象
- exceptions 让错误跨帧传播,stack unwinding 只可靠清理已交给 RAII 的资源
- catch 应位于有恢复/翻译策略的边界,并按派生到基类顺序以 const reference 捕获
- RTTI 支持多态动态类型查询;dynamic_cast 指针失败为空、引用失败抛 bad_cast
- type cast operators 各有不同证明责任;频繁向下转换应反查基类 virtual protocol
练习
- 问题 1:收窄 friend。 Serializer 类只有
writeHeader()需要 Packet 私有字段,应授权整个 Serializer 吗?
- 问题 2:验证栈展开。 parse 抛出时函数持有 ifstream、vector 和裸
new[],哪些会自动清理?
- 问题 3:选择 cast。 Base* 可能指向 Derived,怎样受检访问 Derived 能力,失败语义是什么?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 友元类
- 全部成员获另一个类私有访问权的具名类。
- 友元成员
- 被另一个类单独授权私有访问的成员函数。
- 嵌套类
- 名字位于外层类作用域、对象仍独立的类。
- 异常
- 由 throw 建立并传播到匹配 catch 的错误对象与控制流。
- 栈展开
- 异常传播时逐帧退出并析构已构造自动对象的过程。
- 运行时类型识别
- 查询多态对象动态类型或执行受检转换的机制。