**标题:深入解析 C++17 中的 std::shared_ptr 与 std::unique_ptr:如何正确选择与使用**

在现代 C++ 编程中,智能指针已成为管理资源的核心工具。尤其是 std::unique_ptrstd::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. 常见错误与防范

  1. 循环引用
    shared_ptr 之间相互持有导致引用计数永不归零,内存泄漏。
    解决:使用 std::weak_ptr 打破循环。

  2. 使用裸指针析构
    手动 delete 对象而不让 shared_ptr 管理,导致 double free。
    解决:始终使用智能指针管理资源。

  3. 多线程共享 unique_ptr
    unique_ptr 非线程安全,若多个线程同时持有同一 unique_ptr 会产生悬空引用。
    解决:将 unique_ptr 交给单线程或使用 shared_ptr 做线程共享。

  4. 异常安全
    通过 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。牢记异常安全和线程安全原则,即可让代码更健壮、可维护。

发表评论