第四章 RAII、智能指针与资源管理
第四章 RAII、智能指针与资源管理
1. 什么是 RAII?它为什么是 C++ 资源管理的核心?
问题分析
这道题考查能否从可观察现象追溯到所有权、资源释放与异常安全中的具体机制,并说明规则被破坏后的后果。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:RAII 即“资源获取即初始化”:对象在构造时取得资源,在析构时释放资源。
- 再讲机制:围绕“RAII 解决什么问题? → 设计原则”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合所有权、资源释放与异常安全说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯 C++ 客户端一面
问题讲解
一、RAII 解决什么问题?
RAII 即“资源获取即初始化”:对象在构造时取得资源,在析构时释放资源。栈对象离开作用域会确定性析构,所以正常返回、提前返回和异常展开都能经过同一清理路径。资源可以是内存、文件、锁、Socket、事务或临时状态。

二、设计原则
一个 RAII 类型应明确拥有资源,构造成功后保持有效不变式,析构函数不抛异常。若资源不可复制,应删除复制操作并支持移动;若资源能自然由标准成员管理,优先 Rule of 0,不手写析构。RAII 不等于“对象一定在栈上”,unique_ptr 本身可位于任何存储区。
三、常见错误
- 只把 RAII 理解为智能指针。
- 构造后还需要单独调用
init(),使对象存在半初始化状态。 - 在析构函数中抛异常。
- 手工
lock()后存在提前返回,忘记unlock()。
回答自检
- 获取与释放如何绑定到对象生命周期。
- RAII 如何覆盖异常和提前返回。
- 所有权、不变式和确定性析构的关系。
- 为什么 RAII 不只管理内存。
- RAII 类型复制与移动策略如何决定。
面试官可能追问的问题
- RAII 与 GC 有何不同? RAII 按作用域确定性释放,GC 主要追踪内存可达性,回收时机通常不确定。
- 构造过程中抛异常会怎样? 已构造的成员和基类会析构,对象自身析构不会执行。
- 为何析构应
noexcept? 栈展开时再次抛异常会导致std::terminate。
2. C++ 中常见的智能指针有哪些?分别适合什么场景?
问题分析
这道题不只是让你罗列名词,而是考查能否从所有权、资源释放与异常安全做出有依据的比较和选择。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:手动
new/delete在提前返回、异常和跨对象传递时容易产生泄漏、重复释放和悬空访问。 - 再讲机制:围绕“智能指针解决什么问题? → 三类智能指针 → 选择顺序与边界”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合所有权、资源释放与异常安全说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯 C++ 客户端一面
问题讲解
一、智能指针解决什么问题?
手动 new/delete 在提前返回、异常和跨对象传递时容易产生泄漏、重复释放和悬空访问。智能指针利用 RAII,把动态对象的释放绑定到管理对象的析构。它们主要表达所有权;裸指针和引用仍适合表达短期借用。
二、三类智能指针
unique_ptr<T> 表示唯一所有权,不能复制但可以移动,适合工厂返回值、类成员资源和多态独占对象。shared_ptr<T> 让多个所有者共同决定生命周期,通常通过控制块维护强、弱引用计数。weak_ptr<T> 不拥有对象,用于观察、缓存或打破循环引用,访问前应调用 lock() 获得临时强所有权。
| 类型 | 所有权 | 复制 | 典型场景 | 主要成本/风险 |
|---|---|---|---|---|
unique_ptr | 唯一 | 否,可移动 | 默认动态对象、工厂返回 | 几乎等同裸指针访问 |
shared_ptr | 共享 | 是 | 异步任务、真实共享模型 | 控制块、原子计数、循环引用 |
weak_ptr | 非拥有观察 | 是 | 反向关系、观察者、缓存 | 使用前可能已过期 |
三、选择顺序与边界
优先普通局部对象;必须动态分配时优先 unique_ptr;只有业务上确实存在共同所有权才使用 shared_ptr。函数只是访问对象时不必传智能指针。不同 shared_ptr 实例可并发复制和销毁,但这不让同一个智能指针变量或对象成员自动线程安全。
四、完整可执行示例
示例同时覆盖唯一所有权转移、共享控制块、弱引用观察和 lock()。父节点拥有子节点,子节点只弱引用父节点,因此不会形成强引用环。
#include <iostream>
#include <memory>
#include <string>
#include <utility>
struct Node {
explicit Node(std::string name) : name(std::move(name)) {}
~Node() { std::cout << "destroy " << name << '\n'; }
std::string name;
std::shared_ptr<Node> child;
std::weak_ptr<Node> parent;
};
int main() {
auto unique = std::make_unique<int>(42);
auto owner = std::move(unique);
std::cout << "unique value: " << *owner << '\n';
auto root = std::make_shared<Node>("root");
root->child = std::make_shared<Node>("child");
root->child->parent = root;
if (const auto parent = root->child->parent.lock()) {
std::cout << "parent: " << parent->name
<< ", strong owners: " << parent.use_count() << '\n';
}
}
五、常见错误
- 用同一个裸指针构造两个独立控制块,造成重复释放。
- 使用
shared_ptr(this)绕过已有控制块。 - 使用
shared_ptr掩盖本应唯一的所有权。 - 先检查
expired()再访问,留下检查与使用之间的竞态。 - 认为
shared_ptr会让对象本身线程安全。
回答自检
- 三类智能指针表达的所有权。
- 为什么默认优先
unique_ptr。 - 控制块、强引用和弱引用的作用。
- 循环引用如何形成和打破。
- 智能指针与普通借用的区别。
shared_ptr的线程安全边界。
面试官可能追问的问题
auto_ptr为什么淘汰? 复制会意外转移所有权,C++17 已删除,应使用unique_ptr。unique_ptr能否用于多态? 可以;经基类指针销毁时基类通常需虚析构。- 借用参数是否传
shared_ptr? 不需要延长所有权时传引用或裸指针。 - 为何
lock()优于expired()后再构造?lock()原子地完成检查与获取临时强所有权。
3. unique_ptr 如何表达唯一所有权?如何转移所有权?
问题分析
这道题考查能否把所有权、资源释放与异常安全转化为可执行的判断步骤,而不是只报出 API 名称或最终结论。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:同一时刻只有一个
unique_ptr负责释放对象,因此复制构造和复制赋值被删除。 - 再讲机制:围绕“唯一所有权模型 → 常用操作”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合所有权、资源释放与异常安全说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:字节 C++ 后端一面
问题讲解
一、唯一所有权模型
同一时刻只有一个 unique_ptr 负责释放对象,因此复制构造和复制赋值被删除。std::move 把源指针转换为可移动表达式,移动后目标接管资源,成功移动构造后源 unique_ptr 为空,目标接管原指针;这里不是“通常为空”的实现猜测。参见unique_ptr 移动构造契约。离开作用域、reset() 或被重新赋值时,当前资源通过删除器释放。

二、常用操作
get() 只借出地址,不转移所有权;release() 放弃所有权并返回裸指针,调用者必须重新接管;reset(p) 先释放旧对象再接管新地址。数组使用 unique_ptr<T[]>。自定义删除器可管理 FILE* 等资源,删除器类型会影响 unique_ptr 的类型和可能的大小。
#include <cstdio>
#include <iostream>
#include <memory>
struct FileCloser {
void operator()(std::FILE* file) const noexcept {
if (file) std::fclose(file);
}
};
int main() {
auto value = std::make_unique<int>(42);
auto moved = std::move(value);
std::unique_ptr<std::FILE, FileCloser> file(std::tmpfile());
if (file) std::fputs("RAII\n", file.get());
std::cout << std::boolalpha << static_cast<bool>(value)
<< ' ' << *moved << '\n';
}

三、常见错误
- 移动后继续解引用源
unique_ptr。 - 对
get()返回的地址调用delete。 - 调用
release()后没有建立新的所有者,造成泄漏。 - 用
unique_ptr<Base>管理派生对象,但基类析构非虚且使用默认删除器。
回答自检
- 唯一所有权如何由类型系统强制。
- 移动前后的所有者变化。
get、release、reset的区别。- 数组特化和自定义删除器的用途。
- 多态删除的析构边界。
面试官可能追问的问题
- 为什么函数可以按值返回
unique_ptr? 返回值会移动或被拷贝省略。 - 如何把它传入接管所有权的函数? 参数按值接收,调用处使用
std::move。 - 能否放进 vector? 可以,容器在需要时移动元素。
4. shared_ptr 的控制块和引用计数如何工作?
问题分析
这道题考查能否从可观察现象追溯到所有权、资源释放与异常安全中的具体机制,并说明规则被破坏后的后果。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:最小实现需要一个句柄和一个控制块。
- 再讲机制:围绕“手写
shared_ptr要先拆开两个责任 → 控制块保存什么? → 标准语义与实现边界”说明规则如何生效,把关键对象、时机或状态变化串起来。 - 最后讲边界:结合所有权、资源释放与异常安全说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:美团 C++ 后端二面
问题讲解
补充:手写 shared_ptr 要先拆开两个责任
最小实现需要一个句柄和一个控制块。句柄保存对象地址,控制块保存强引用计数、删除动作和必要的弱引用信息;复制句柄只增加计数,最后一个强引用负责销毁对象。真正可用的实现还必须处理自赋值、移动、异常安全、数组和自定义删除器,不能把“一个整数计数器加一减一”当成完整的线程安全 shared_ptr。
计数归零和对象销毁必须使用与并发模型匹配的原子操作;即使引用计数安全,也不代表被管理对象的成员读写安全。
一、控制块保存什么?
常见实现的控制块包含强引用计数、弱引用计数、删除器以及分配器信息。复制 shared_ptr 增加强引用,销毁副本减少强引用;最后一个强引用消失时销毁被管理对象,最后一个弱引用也消失后才释放控制块。

二、标准语义与实现边界
引用计数操作通常需要线程安全,但标准不要求计数具体字段或单一内存布局。make_shared 常合并对象和控制块为一次分配;只要弱引用仍在,合并块可能仍保留,尽管对象已经析构。别名构造允许“拥有一个对象、指向其子对象”,所以 get() 相同不代表控制块相同,get() 不同也不代表所有权不同。
三、危险构造方式
同一裸指针分别交给两个 shared_ptr 会创建两个控制块,最终重复删除。对象需要从 this 获得共享所有权时应继承 enable_shared_from_this,且只有对象已经由 shared_ptr 管理后才能调用 shared_from_this()。
四、常见错误
- 把引用计数称为垃圾回收,并认为它能自动处理环。
- 用
use_count()做并发业务判断。 - 从
this直接创建新的shared_ptr。 - 认为
make_shared在所有场景都更省内存。
回答自检
- 三个对象各自的生命周期。
- 强引用和弱引用的归零行为。
make_shared合并分配的利弊。- 双控制块为什么会重复释放。
- 别名构造为何使“地址”和“所有权”不同。
面试官可能追问的问题
- 控制块何时释放? 强引用归零销毁对象,强弱计数均满足释放条件后释放控制块。
- 引用计数为何有成本? 复制/销毁常需原子更新,还会增加间接访问和缓存一致性流量。
enable_shared_from_this如何工作? 它保存与既有控制块关联的弱引用,再从中获得共享指针。
5. weak_ptr 如何解决循环引用?lock() 为什么重要?
问题分析
这道题考查能否从可观察现象追溯到所有权、资源释放与异常安全中的具体机制,并说明规则被破坏后的后果。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:若父对象和子对象互相保存
shared_ptr,即使外部所有者消失,环内强引用计数仍不为零。 - 再讲机制:围绕“循环引用为何泄漏 → 为什么直接调用
lock()”说明规则如何生效,把关键对象、时机或状态变化串起来。 - 最后讲边界:结合所有权、资源释放与异常安全说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯社交客户端一面
问题讲解
一、循环引用为何泄漏
若父对象和子对象互相保存 shared_ptr,即使外部所有者消失,环内强引用计数仍不为零。weak_ptr 观察同一控制块但不增加强引用,因此应把反向关系、缓存或观察者关系建模为弱边。

二、为什么直接调用 lock()
lock() 在对象仍存活时返回一个临时 shared_ptr,并在这一步建立强所有权;若对象已销毁则返回空。先调用 expired() 再做其他操作存在检查与使用之间的时间窗口,另一个线程可能让最后一个强引用消失。

三、常见错误
- 任意把环的一边改成弱引用,却没有分析真实所有权方向。
- 通过
weak_ptr像裸指针一样直接访问对象。 expired()返回 false 后认为后续必然安全。- 把
weak_ptr当作无需同步的数据结构。
回答自检
- 强引用环为什么无法归零。
- 哪类关系应该是非拥有边。
lock()的检查并获取语义。- 对象与控制块释放时机的差别。
weak_ptr不能替代一般线程同步。
面试官可能追问的问题
- 弱引用会延长控制块寿命吗? 会延长控制块,但不延长对象寿命。
- 缓存为何适合 weak_ptr? 缓存可复用仍存活对象,又不强迫对象因缓存而存活。
- 观察者一定用 weak_ptr 吗? 仅当被观察者由
shared_ptr管理;其他模型可用连接令牌或显式注销。
6. make_unique、make_shared 和直接使用 new 有什么区别?
问题分析
这道题不只是让你罗列名词,而是考查能否从所有权、资源释放与异常安全做出有依据的比较和选择。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:
make_unique<T>(args...)直接构造unique_ptr<T>,避免裸指针短暂暴露并减少类型重复。 - 再讲机制:围绕“默认选择 → 例外场景”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合所有权、资源释放与异常安全说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:百度 C++ 后端一面
问题讲解
一、默认选择
make_unique<T>(args...) 直接构造 unique_ptr<T>,避免裸指针短暂暴露并减少类型重复。make_shared<T> 常把控制块和对象合并为一次分配,提高局部性和分配效率。直接 shared_ptr<T>(new T) 通常至少需要对象与控制块两次分配。

示意常见实现的分配布局,不是标准规定的固定分配次数。
二、例外场景
需要自定义删除器时,make_unique 不能直接指定删除器,通常显式构造 unique_ptr。对象很大且弱引用可能长期存活时,分离对象和控制块可让对象存储更早归还。构造函数非 public 时,可通过合适的工厂设计处理访问控制,不能简单认为 make_shared 总能访问私有构造。
三、常见错误
- 认为
make_shared的一次分配是标准强制。 - 为获取裸指针而先
new,再到更远处包装智能指针。 - 把同一裸指针交给多个智能指针构造函数。
- 认为所有智能指针创建都应机械使用
make_shared。
回答自检
- 两个 make 工厂的所有权收益。
make_shared常见合并分配模型。- 合并分配与弱引用的内存权衡。
- 自定义删除器等例外场景。
- 为什么应缩短裸指针暴露窗口。
面试官可能追问的问题
- C++11 有
make_unique吗? 标准库从 C++14 提供。 make_shared如何改善异常安全? 所有权在单个表达式内建立,避免裸资源暴露。- 自定义分配器怎么办? 可使用
allocate_shared。
7. 什么是 Rule of 0、Rule of 3 和 Rule of 5?
问题分析
这道题考查是否建立了所有权、资源释放与异常安全的清晰概念边界,并能把类别、职责和限制对应起来。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:Rule of 3:若类需要自定义析构、复制构造或复制赋值中的一个,往往需要一起审视三个,因为类可能直接管理资源。
- 再讲机制:围绕“三条经验规则 → 重要边界”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合所有权、资源释放与异常安全说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:腾讯 C++ 客户端二面
问题讲解
一、三条经验规则
Rule of 3:若类需要自定义析构、复制构造或复制赋值中的一个,往往需要一起审视三个,因为类可能直接管理资源。C++11 加入移动后形成 Rule of 5。Rule of 0 更推荐:让 string、vector、智能指针等成员管理资源,业务类不手写任何特殊成员。
二、重要边界
这些是设计经验,不是语法规则。用户声明析构函数会影响移动操作的隐式生成;成员是否可复制、可移动也会决定类的隐式特殊成员是否删除。多态基类常需要虚析构,若要保留复制/移动语义,应显式 = default 或根据设计删除。
三、常见错误
- 看到 Rule of 5 就为每个类手写五个函数。
- 手写析构释放裸指针,却保留浅复制。
- 移动后没有让源对象保持可析构、可赋值状态。
- 忽略自定义析构对隐式移动生成的影响。
回答自检
- 三条规则各自的背景。
- 特殊成员之间为何相互影响。
- 浅复制资源类为何危险。
default与delete的设计含义。- 如何通过组合实现 Rule of 0。
面试官可能追问的问题
- 什么时候
= default? 希望明确采用编译器生成语义或恢复被其他声明抑制的操作时。 - 什么时候
= delete? 类型语义不允许复制或某种构造时。 - 为什么 Rule of 0 最好? 资源细节集中在成熟 RAII 成员中,组合语义更容易正确。
8. 拷贝构造、拷贝赋值、移动构造和移动赋值有什么区别?
问题分析
这道题不只是让你罗列名词,而是考查能否从所有权、资源释放与异常安全做出有依据的比较和选择。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:构造函数用于初始化一个尚不存在的新对象;
- 再讲机制:围绕“四种操作 → 实现要求”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合所有权、资源释放与异常安全说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:美团客户端一面
问题讲解
一、四种操作
构造函数用于初始化一个尚不存在的新对象;赋值运算符作用于已经完成构造的对象,必须先妥善处理其旧状态。拷贝保留源值并建立独立的目标语义,移动可以窃取源资源,标准库类型除另有规定外,移动后处于有效但未指定的状态;用户自定义类型的移动后承诺由其接口定义,应保证文档允许的析构和后续操作安全。参见标准库移动后状态。
| 操作 | 目标初始状态 | 典型参数 | 源对象 |
|---|---|---|---|
| 拷贝构造 | 尚未构造 | const T& | 保持原值 |
| 拷贝赋值 | 已有资源 | const T& | 保持原值 |
| 移动构造 | 尚未构造 | T&& | 有效但状态可变 |
| 移动赋值 | 已有资源 | T&& | 有效但状态可变 |

二、实现要求
资源类型的拷贝赋值要处理自赋值,并尽量先取得新资源再提交,避免失败后破坏旧值。移动操作通常标记 noexcept,容器扩容时才更愿意移动而不是为了强异常保证回退到复制。能使用 Rule of 0 时无需手写。
三、常见错误
- 把
T b = a;当成先默认构造再赋值。 - 移动赋值前忘记释放目标旧资源。
- 假设移动后源对象一定为空;仅某些库类型提供明确状态保证。
- 自定义移动构造却无条件标成
noexcept,内部实际可能抛异常。
回答自检
- 构造与赋值的目标状态差异。
- 拷贝和移动对源对象的不同承诺。
- 移动后“有效但未指定”的含义。
- 自赋值和异常安全如何处理。
noexcept为什么影响容器选择。
面试官可能追问的问题
- 为什么拷贝参数通常是
const T&? 避免按值递归调用拷贝,并允许复制 const 对象。 - 什么是 copy-and-swap? 先按值取得副本,再交换状态,容易提供强异常保证。
- 移动一定比拷贝快吗? 不一定,小对象或内联存储可能成本接近。
9. 异常安全的基本保证、强保证和不抛保证是什么?
问题分析
这道题考查是否建立了所有权、资源释放与异常安全的清晰概念边界,并能把类别、职责和限制对应起来。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:基本保证:异常后没有资源泄漏,对象仍满足不变式,但值可能变化。
- 再讲机制:围绕“三种保证 → 如何实现”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合所有权、资源释放与异常安全说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:百度基础架构二面
问题讲解
一、三种保证
基本保证:异常后没有资源泄漏,对象仍满足不变式,但值可能变化。强保证:操作要么成功,要么对象保持调用前状态,类似事务。无抛保证:操作承诺不传播异常,通常以 noexcept 表达。另有“无保证”状态,异常后对象甚至可能不再满足可用不变式。
二、如何实现
先用 RAII 临时对象完成可能失败的工作,再通过不抛的 swap 或状态替换提交,可以实现强保证。析构和释放操作必须尽可能不抛。noexcept 是接口承诺,不是“忽略异常”:若异常逃出 noexcept 函数,程序调用 std::terminate。

三、常见错误
- 把“捕获所有异常”误认为异常安全。
- 只保证不泄漏,却声称提供强保证。
- 为了性能随意添加
noexcept。 - 在对象状态修改一半后调用可能抛出的操作。
回答自检
- 三层异常保证的区别。
- 对象不变式和资源不泄漏的关系。
- 临时状态加提交如何实现强保证。
noexcept失败时的行为。- 为什么保证级别必须按具体操作说明。
面试官可能追问的问题
- 标准容器都提供强保证吗? 视操作、元素类型和分配器而定,需查具体接口保证。
- 析构函数抛异常会怎样? 若正处于栈展开,第二个异常会终止程序。
swap为何常用于提交? 它可把准备好的状态以不抛操作替换进去。
10. 如何使用 RAII 管理文件、锁和其他非内存资源?
问题分析
这道题考查能否把所有权、资源释放与异常安全转化为可执行的判断步骤,而不是只报出 API 名称或最终结论。
建议按“结论 → 机制 → 边界”组织回答:
- 先给结论:任何具有“获取/释放”配对的资源都能使用 RAII。
- 再讲机制:围绕“通用包装模式 → 边界与选择”说明规则如何生效,把关键对象、时机或状态变化串起来。
- 最后讲边界:结合所有权、资源释放与异常安全说明适用条件、常见误用,以及如何通过代码、编译结果或运行现象验证判断。
- 来源:阿里云存储一面
问题讲解
一、通用包装模式
任何具有“获取/释放”配对的资源都能使用 RAII。构造函数取得有效句柄,析构函数调用对应释放函数;复制要么被禁止,要么有明确的复制协议;移动用于转移所有权。标准库已提供 fstream、lock_guard、unique_lock 等类型,优先复用。
二、完整示例
#include <cstdio>
#include <memory>
#include <mutex>
#include <stdexcept>
struct FileCloser {
void operator()(std::FILE* file) const noexcept {
if (file) std::fclose(file);
}
};
int main() {
std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);
std::unique_ptr<std::FILE, FileCloser> file(std::tmpfile());
if (!file) throw std::runtime_error("cannot create temporary file");
std::fputs("resource is owned\n", file.get());
}
这里 lock_guard 先构造、文件所有者后构造,因此离开作用域时文件先关闭、锁后释放。逆序析构常用于表达资源依赖。

失败分支按异常能传播至匹配catch并进行栈展开的教学前提绘制,不覆盖所有未捕获异常终止路径。
三、边界与选择
句柄不一定是指针,例如文件描述符可能用整数且 -1 表示无效,这时自定义小型移动类通常比勉强塞入指针智能指针更清楚。关闭操作若失败,析构函数不能靠抛异常报告,应在显式 close() 中报告或记录错误,同时让析构提供兜底。
四、常见错误
- 手动获取锁后在多个分支手动释放。
- 复制唯一句柄,导致重复关闭。
- 析构中抛出关闭失败异常。
- 错误安排局部变量声明顺序,导致依赖资源先被销毁。
回答自检
- 如何识别可 RAII 化的成对 API。
- 构造、移动、析构分别承担什么职责。
- 逆序析构如何处理资源依赖。
- 非指针句柄为什么适合自定义包装类。
- 析构不能抛异常时如何报告关闭错误。
面试官可能追问的问题
lock_guard与unique_lock区别? 前者简单固定持锁;后者支持延迟加锁、解锁和条件变量。- 关闭失败怎么报告? 提供显式关闭操作返回错误,析构仅做不抛兜底。
- 资源句柄可复制怎么办? 只有底层协议定义了复制/引用计数时才实现相应语义。




