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_casttypeid 与其他 type cast operators 的证明责任

机制总览

Chapter 15:Friends, Exceptions, and More:机制路径

  1. 1

    为什么访问授权、错误传播和类型探测都需要窄边界

    friend、exception 与 RTTI 看似是三个附加主题,实际都在跨越普通局部边界:friend 跨 private 访问,throw 跨调用帧返回, dynamic cast 跨静态类型查看动态类型。越界能力越强,越要限制授予对象、传播范围和失败语义。

  2. 2

    friend class 适合紧密但明确的协作

    友元类(friend class)让被指定类的所有成员访问授权类的 private/protected 数据。友元关系不对称、不传递、不继承:A 把 B 设为 friend,不代表 A 能访问 B,也不代表 B 的派生类自动获得权限。

  3. 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 的派生类自动获得权限。

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)只授权另一个类的指定成员。它减少意外依赖,但需要安排前置声明、类定义和成员定义顺序,让双方类型在使用点完整或已声明。

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。

friendship 是具名授权,不继承、不传递、也不自动对称;优先选择能完成协作的最窄粒度。

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。

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 共用析构路径。

栈展开只自动清理已经成功构造的自动/RAII 对象;裸资源若未交给对象,跨 throw 仍会泄漏。

handler 是策略边界,不是每层都要捕获

能修复、翻译语义、补充上下文或记录后终止的一层才 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_casttypeid 提供。对 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 都应回答源/目标类型、可能损失、运行时检查、对齐/生命周期和失败处理。

三步审计跨边界能力

分步1 / 3

第一步:画最窄访问授权

列出 friend function/class/member 与 nested helper 实际需要的数据,验证非对称、不传递和声明顺序。

friendship 是具名授权,不继承、不传递、也不自动对称;优先选择能完成协作的最窄粒度。

小结

  • friend classes and members 是具名、单向、非传递授权;nested class 不自动绑定外层对象
  • exceptions 让错误跨帧传播,stack unwinding 只可靠清理已交给 RAII 的资源
  • catch 应位于有恢复/翻译策略的边界,并按派生到基类顺序以 const reference 捕获
  • RTTI 支持多态动态类型查询;dynamic_cast 指针失败为空、引用失败抛 bad_cast
  • type cast operators 各有不同证明责任;频繁向下转换应反查基类 virtual protocol

练习

  1. 问题 1:收窄 friend。 Serializer 类只有 writeHeader() 需要 Packet 私有字段,应授权整个 Serializer 吗?
  1. 问题 2:验证栈展开。 parse 抛出时函数持有 ifstream、vector 和裸 new[],哪些会自动清理?
  1. 问题 3:选择 cast。 Base* 可能指向 Derived,怎样受检访问 Derived 能力,失败语义是什么?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

友元类
全部成员获另一个类私有访问权的具名类。
友元成员
被另一个类单独授权私有访问的成员函数。
嵌套类
名字位于外层类作用域、对象仍独立的类。
异常
由 throw 建立并传播到匹配 catch 的错误对象与控制流。
栈展开
异常传播时逐帧退出并析构已构造自动对象的过程。
运行时类型识别
查询多态对象动态类型或执行受检转换的机制。

讨论

评论区加载中…