【读书笔记】《Effective Modern C++》第4章 Smart Pointers
《Effective Modern C++》第4章 Smart Pointers
一、使用 std::unique_ptr 管理独占所有权(Item 18)
-
基本概念
std::unique_ptr 提供对动态分配资源的独占所有权管理。与裸指针不同,unique_ptr 在离开作用域或被重置时自动释放所管理的资源。它禁止拷贝,只能移动。 -
使用方式
推荐工厂函数方式创建:auto up = std::make_unique<Foo>(arg1, arg2);
当需要转移所有权时:
std::unique_ptr<Foo> up2 = std::move(up); // 此时 up 为空,up2 拥有资源
-
自定义删除器
对于非 new 分配的资源,如 FILE* 或自定义句柄,可传入删除器类型:std::unique_ptr<FILE, decltype(&fclose)> fp(fopen("data.txt", "r"), &fclose);
删除器在 unique_ptr 析构时被调用,保证资源释放。
-
注意事项
- 禁止拷贝可防止重复释放;
- 作为成员变量时,可用 = default 或手动定义移动构造/赋值;
- 不要将裸指针与 unique_ptr 混合管理同一资源。
二、使用 std::shared_ptr 管理共享所有权(Item 19)
-
基本概念
std::shared_ptr 通过内部控制块维护引用计数,允许多方共享同一对象。最后一个 shared_ptr 析构时,自动释放资源。 -
使用方式
推荐工厂函数:auto sp1 = std::make_shared<Foo>(arg); auto sp2 = sp1; // 引用计数增至 2 sp1.reset(); // 计数减至 1 // 最后 sp2 离开作用域时释放 Foo
-
性能与成本
- 控制块与对象默认一次分配;
- 引用计数操作是原子级别开销;
- 拷贝和析构时都要修改计数。
-
循环引用问题
当对象 A 和 B 相互持有 shared_ptr,会导致引用计数永不归零。必须通过 weak_ptr 打破循环。
三、使用 std::weak_ptr 管理可能失效的观察型指针(Item 20)
-
基本概念
std::weak_ptr 是对 shared_ptr 管理资源的非拥有型引用,不增加引用计数。它记录 control block 的弱引用,可检测资源是否已释放。 -
使用方式
std::shared_ptr<Foo> sp = std::make_shared<Foo>(); std::weak_ptr<Foo> wp = sp; // 计数不变if (auto guard = wp.lock()) {// guard 为 shared_ptr,安全访问guard->method(); } else {// 对象已销毁 }
-
应用场景
- 观察者模式、缓存实现、回调注册时,不希望延长对象生命周期;
- 打破 shared_ptr 循环引用的一端。
-
注意事项
- 使用 lock() 获取 shared_ptr 并检查是否为空;
- 避免直接将 weak_ptr 转为裸指针;
- expired() 可用于简单过期检测,但 lock() 更安全。
四、优先使用 std::make_unique 和 std::make_shared(Item 21)
-
问题及原因
直接 new 后再构造智能指针存在两次内存分配和潜在异常安全漏洞:一旦在 new 后抛出,容易泄漏。 -
make 工厂函数优势
- 单次内存分配:make_shared 在一次分配中创建控制块和对象;
- 异常安全:资源在构造过程中即被智能指针管理,不会泄漏。
-
示例对比
// 不推荐 std::unique_ptr<Foo> up(new Foo(args)); std::shared_ptr<Foo> sp(new Foo(args));// 推荐 auto up2 = std::make_unique<Foo>(args); auto sp2 = std::make_shared<Foo>(args);
-
特殊情况
- 需要自定义删除器时,仍需手动 new 并传入删除器;
- make_shared 无法分离控制块内存,若希望控制释放时机可考虑手动创建。
五、Pimpl 习惯用法中在实现文件定义特殊成员函数(Item 22)
-
Pimpl 模式目的
隐藏实现细节、减少头文件依赖、加快编译速度。头文件中声明不完整类型,实际数据保存在实现类 Impl 中。 -
头文件示例(Widget.h)
class Widget { public:Widget();~Widget();Widget(const Widget&);Widget& operator=(const Widget&); private:struct Impl;std::unique_ptr<Impl> pImpl; };
-
实现文件示例(Widget.cpp)
struct Widget::Impl {// 数据成员 };Widget::Widget() : pImpl(std::make_unique<Impl>()) {} Widget::~Widget() = default; Widget::Widget(const Widget& w): pImpl(std::make_unique<Impl>(*w.pImpl)) {} Widget& Widget::operator=(const Widget& w) {*pImpl = *w.pImpl;return *this; }
-
原因与好处
- Impl 定义对外部不可见,确保封装性;
- 修改 Impl 后只需重新编译 cpp,减少依赖传播;
- 特殊成员函数在 cpp 中定义时,Impl 完整可见,避免不完整类型使用错误。
总结
本章围绕智能指针的五个核心要点展开,从独占所有权到共享所有权,再到观察型 weak_ptr,以及工厂函数与 Pimpl 相结合的模式实践,全面阐明了现代 C++ 中资源管理的最佳实践。通过合理选用 unique_ptr、shared_ptr 和 weak_ptr,并配合 make_xxx 工厂函数和 Pimpl 隐藏实现细节,能够编写安全、高效且易于维护的 C++ 应用。