RAII模板类必须接管原始指针的生命周期,核心是构造时获取资源、析构时释放资源,禁用拷贝、默认移动、支持自定义删除器,并严格区分release()(移交所有权)与reset()(释放并可选接管新资源),优先使用std::unique_ptr而非自行实现。

RAII模板类必须接管原始指针的生命周期
RAII不是“加个析构函数就行”,核心是把资源获取(acquire)和释放(release)绑定到对象的构造与析构上,且禁止资源被意外复制或丢失。一旦你用 new 分配了内存,就必须确保:构造成功 → 资源归属该对象;构造失败 → 不留泄漏;析构必然执行 → 资源被释放。
常见错误是手动传入裸指针后不置空、不禁止拷贝:
template<typename T>
class AutoPtr {
T* ptr_;
public:
explicit AutoPtr(T* p) : ptr_(p) {} // 危险!没检查p是否为nullptr
~AutoPtr() { delete ptr_; } // 危险!可能多次delete同一地址
AutoPtr(const AutoPtr& other) : ptr_(other.ptr_) {} // 危险!浅拷贝导致double free
};正确做法是:默认移动语义、禁用拷贝、构造时接管所有权、析构前判空:
-
ptr_初始化为nullptr,避免未初始化析构 - 拷贝构造函数和
operator=显式= delete - 提供移动构造和移动赋值,转移
ptr_并将原对象置为nullptr - 析构中只对非空
ptr_调用delete
支持自定义删除器才能适配非堆内存和系统资源
RAII的价值不仅限于 new/delete,还要能管 fopen/fclose、malloc/free、Windows CreateFile/CloseHandle 等。硬编码 delete 会锁死用途。
立即学习“C++免费学习笔记(深入)”;
标准做法是用仿函数或函数指针作为模板参数或成员:
template<typename T, typename Deleter = std::default_delete<T>>
class UniqueResource {
T* ptr_;
Deleter del_;
public:
explicit UniqueResource(T* p, Deleter d = Deleter{})
: ptr_(p), del_(std::move(d)) {}
~UniqueResource() { if (ptr_) del_(ptr_); }
UniqueResource(const UniqueResource&) = delete;
UniqueResource& operator=(const UniqueResource&) = delete;
UniqueResource(UniqueResource&& o) noexcept
: ptr_(o.ptr_), del_(std::move(o.del_)) { o.ptr_ = nullptr; }
// ... 其他移动赋值、get()、release() 等
};使用示例:
- 管理
FILE*:UniqueResource<file void>(fp, [](FILE* f){ fclose(f); })</file> - 管理 Windows 句柄:
UniqueResource<handle void>(h, CloseHandle)</handle> - 管理数组:
UniqueResource<int std::default_delete>>(new int[10])</int>
release() 和 reset() 的语义必须严格区分
用户常混淆这两个接口:一个只是移交所有权(不释放),一个强制释放并可选接管新资源。错用会导致提前释放或悬空指针。
release() 应仅返回当前指针并置 ptr_ 为 nullptr,不做任何释放动作;reset(T* p = nullptr) 则先释放旧资源(调用删除器),再接管 p:
- 调用
release()后,原对象不再管理资源,但资源仍存活 —— 你得自己负责后续delete或关闭 - 调用
reset(nullptr)等价于“释放当前资源并清空”,适合中途放弃 - 调用
reset(new T)会先释放旧资源,再接管新资源 —— 安全替换,无泄漏
注意:若删除器有状态(如捕获了某个上下文),reset() 替换指针时应保持删除器不变;而 release() 不影响删除器。
别直接重实现 unique_ptr,优先用标准库 + 自定义删除器
除非你在写嵌入式无 STL 环境、或教学演示,否则不要从零写 RAII 智能指针。C++11 起 std::unique_ptr 已覆盖绝大多数场景,且经过充分测试和优化。
它的优势在于:
- 移动语义开箱即用,拷贝被禁用
- 支持数组特化:
std::unique_ptr<int></int> - 支持自定义删除器类型推导(包括 lambda,只要不捕获)
- 与容器、算法无缝协作(如
std::vector<:unique_ptr>></:unique_ptr>)
真正需要自研的情况极少:比如要集成特定日志钩子、做资源计数调试、或目标平台连 <memory></memory> 都不可用。此时才基于上述三原则动手,而不是为了“练手”绕过 std::unique_ptr。
最易被忽略的一点:自定义删除器若含捕获(如 [ctx](void* p){ log(ctx, p); free(p); }),会导致 std::unique_ptr 无法推导类型,必须显式指定模板参数,且不能用于容器 —— 这时候才值得考虑封装一层薄包装。


















