在现代 C++ 编程中,智能指针已成为管理资源的核心工具。尤其是 std::unique_ptr 和 std::shared_ptr,它们分别提供了独占所有权与共享所有权的机制。本文将从语义、使用场景、线程安全、性能以及常见错误等方面,对这两种智能指针进行深入剖析,并给出实战建议,帮助你在项目中更好地决定使用哪一种。
1. 基础语义回顾
| 指针类型 | 所有权模型 | 可复制性 | 线程安全 | 内存管理 |
|---|---|---|---|---|
| `std::unique_ptr | ||||
| ` | 独占 | 不可复制(可移动) | 仅移动操作是线程安全的 | 自动析构,单线程使用 |
| `std::shared_ptr | ||||
| ` | 共享 | 可复制 | 读/写引用计数线程安全 | 自动析构,引用计数保证 |
- unique_ptr:一次只能有一个指针拥有对象,适合资源所有权单一且生命周期由单个对象决定的情况。
- shared_ptr:多指针可以共享同一对象,适合多方需要访问同一资源且生命周期需要协同管理的场景。
2. 典型使用场景
2.1 unique_ptr
| 场景 | 说明 |
|---|---|
| 资源拥有者 | 对象生命周期与拥有者同在,如工厂函数返回对象的所有权。 |
| 可变对象 | 需要修改指针指向不同对象时,使用 reset() 或 release()。 |
| 结构体成员 | 作为类的私有成员,用于资源管理。 |
| 递归算法 | 递归调用返回值可用 unique_ptr 传递,避免内存泄漏。 |
示例:
std::unique_ptr <File> openFile(const std::string& path) {
auto file = std::make_unique <File>(path);
if (!file->isOpen()) throw std::runtime_error("open fail");
return file; // 移动所有权
}
2.2 shared_ptr
| 场景 | 说明 |
|---|---|
| 事件系统 | 事件监听者共享同一事件对象,避免提前释放。 |
| 缓存 | 共享缓存对象,多个线程同时访问。 |
| 复杂对象图 | 对象之间存在循环引用时需要使用 weak_ptr 防止循环。 |
| 插件/模块 | 模块间共享同一资源实例。 |
示例:
void registerListener(const std::shared_ptr <EventHandler>& handler) {
listeners.push_back(handler);
}
3. 线程安全细节
- unique_ptr:移动构造/赋值是线程安全的;但在同一对象上进行读取/写入时需自行同步。
- shared_ptr:内部引用计数使用原子操作,增减引用计数是线程安全的;但对象本身的状态不是线程安全的,需要同步。
小技巧:
在多线程环境中,如果多个线程需要同时读取对象数据,可以将对象包裹在 std::shared_ptr<const T>,并使用读写锁或 std::atomic<std::shared_ptr<T>>。
4. 性能考量
| 指标 | unique_ptr | shared_ptr |
|---|---|---|
| 复制成本 | 0 | O(1)(原子递增) |
| 析构成本 | 直接析构 | 引用计数递减 + 可能析构 |
| 内存占用 | 1 sizeof(T) | 1 sizeof(T) + 2 * sizeof(std::atomic) |
- 在性能敏感的代码路径中,优先考虑
unique_ptr。 shared_ptr由于引用计数需要原子操作,若高频创建/销毁会产生一定开销。
5. 常见错误与防范
-
循环引用
shared_ptr之间相互持有导致引用计数永不归零,内存泄漏。
解决:使用std::weak_ptr打破循环。 -
使用裸指针析构
手动delete对象而不让shared_ptr管理,导致 double free。
解决:始终使用智能指针管理资源。 -
多线程共享
unique_ptr
unique_ptr非线程安全,若多个线程同时持有同一unique_ptr会产生悬空引用。
解决:将unique_ptr交给单线程或使用shared_ptr做线程共享。 -
异常安全
通过reset()或release()可能导致资源泄漏。
解决:尽量使用std::make_unique/std::make_shared并避免手动delete。
6. 进阶:自定义删除器与计数器
6.1 自定义删除器
struct FileDeleter {
void operator()(File* f) const {
std::cout << "Closing file\n";
delete f;
}
};
std::unique_ptr<File, FileDeleter> filePtr(new File("log.txt"));
6.2 自定义计数器
std::shared_ptr 的计数器可以通过 std::make_shared 的别名模板实现:
template <class T, class Counter = std::atomic<std::size_t>>
class shared_ptr_custom : public std::shared_ptr <T> {
// ...实现细节...
};
7. 结语
unique_ptr:轻量、性能好,适合独占所有权,避免不必要的引用计数开销。shared_ptr:易于共享所有权,但需注意循环引用和线程安全细节。
在实际项目中,先考虑资源拥有模式;若是单一拥有者,使用 unique_ptr;若存在多方共享且生命周期交织,使用 shared_ptr 并配合 weak_ptr。牢记异常安全和线程安全原则,即可让代码更健壮、可维护。