在现代 C++ 编程中,资源管理已从手工 new/delete 过渡到更安全、更易维护的智能指针。std::unique_ptr、std::shared_ptr 与 std::weak_ptr 分别提供了独占、共享与非拥有的指针语义。本文将深入剖析三者的实现原理、使用场景、常见陷阱以及性能考量,为你在实际项目中做出更合理的选择。
1. 资源所有权的三种语义
| 指针类型 | 所有权 | 线程安全 | 典型用途 |
|---|---|---|---|
std::unique_ptr |
独占 | 只在单线程环境下安全 | 临时对象、RAII、工厂函数返回值 |
std::shared_ptr |
共享 | 读/写操作线程安全(引用计数) | 对象生命周期跨越多个所有者 |
std::weak_ptr |
非拥有 | 线程安全 | 防止循环引用、观察者模式 |
2. std::unique_ptr
2.1 基本特性
- 独占所有权:只能有一个
unique_ptr拥有同一原始指针。 - 移动语义:支持
std::move转移所有权,拷贝构造/赋值被禁用。 - 析构时自动删除:在作用域结束时自动调用
delete。
2.2 自定义删除器
struct MyDeleter {
void operator()(int* ptr) const {
std::cout << "Deleting int\n";
delete ptr;
}
};
std::unique_ptr<int, MyDeleter> p(new int(42));
自定义删除器可用于管理非 delete 的资源(如 fclose、munmap 等)。
2.3 与数组配合
std::unique_ptr<int[]> arr(new int[10]); // 自动调用 delete[]
记得使用方括号 [] 指定数组删除器。
2.4 常见误区
- 不要返回裸指针:
unique_ptr的所有权应该通过返回值或引用传递。 - 不要与
std::shared_ptr混用:两者之间可以std::move转换,但会导致性能损耗。
3. std::shared_ptr
3.1 引用计数实现
内部包含:
- 控制块:持有
use_count与weak_count,以及删除器。 - 线程安全:
use_count/weak_count操作使用std::atomic.
class ControlBlock {
public:
std::atomic <size_t> use_count{1};
std::atomic <size_t> weak_count{0};
// ...
};
3.2 典型使用场景
- 跨模块共享:如 GUI 控件、网络连接等资源需要在多处使用。
- 树形结构:父节点和子节点之间可能需要相互引用,使用
shared_ptr+weak_ptr解决循环引用。
3.3 循环引用与 weak_ptr
struct Node {
std::shared_ptr <Node> child;
std::weak_ptr <Node> parent; // 防止循环引用
};
weak_ptr 不会计入引用计数,提供对对象的观察而不持有所有权。
3.4 性能注意
- 控制块分配:`make_shared (args…)` 会一次性分配对象与控制块,减少分配次数。
- 非线程安全的
use_count:如果你不需要线程安全,手动实现自己的计数器可能更快。
4. std::weak_ptr
4.1 作用
- 观察者模式:让对象观察某个资源是否已被销毁。
- 分离生命周期:在需要时通过
lock()转化为shared_ptr,如果对象已被销毁则得到nullptr。
4.2 常见代码
std::weak_ptr <Widget> observer = subject; // subject 为 shared_ptr
if (auto s = observer.lock()) { // s 为 shared_ptr
s->draw();
}
若 subject 已被销毁,observer.lock() 返回空指针,避免悬挂指针。
5. 与 C 风格 API 的互操作
- 从裸指针包装:`std::shared_ptr sptr(rawPtr, [](T* p){ delete p; });`
- 自定义删除器:为
fopen返回的FILE*或mmap分配的内存包装。
auto file = std::shared_ptr <FILE>(fopen("log.txt", "r"), [](FILE* f){ fclose(f); });
6. 常见错误与调试技巧
| 错误 | 现象 | 解决方案 |
|---|---|---|
shared_ptr 循环引用 |
内存泄漏,析构不触发 | 使用 weak_ptr 或 std::weak_ptr |
unique_ptr 拷贝 |
编译错误 | 使用 std::move |
多线程共享 unique_ptr |
数据竞争 | 避免跨线程共享,或使用 shared_ptr |
weak_ptr 失效 |
空指针 | 确认资源已被销毁后再使用 |
调试时可使用 std::enable_shared_from_this 或 std::shared_ptr 的 use_count() 检查引用计数。
7. 性能优化小贴士
std::make_shared:一次性分配,减少内存碎片。- 预分配:在大对象构造前使用
operator new与std::unique_ptr结合,可降低分配次数。 - 自定义控制块:对于极端场景,可手写轻量级控制块减少锁开销。
- 延迟初始化:
std::shared_ptr结合std::lazy或std::optional,避免不必要的计数器操作。
8. 小结
unique_ptr:最轻量、最安全,适合独占所有权的场景。shared_ptr:支持共享生命周期,但需注意循环引用与性能开销。weak_ptr:用于观察或解除循环引用,配合shared_ptr使用。
在实际开发中,合理选择指针类型、注意线程安全与性能权衡,能让代码更简洁、可维护且安全。希望本文能为你在 C++ 资源管理上提供实用的参考与启发。