C++ 中的智能指针之 std::unique_ptr 与 std::shared_ptr 的区别与使用场景

在 C++11 之后,智能指针成为了管理动态内存的重要工具。最常用的两种智能指针是 std::unique_ptr 和 std::shared_ptr。它们虽然都可以自动释放资源,但在所有权模型、引用计数、性能以及使用场景上存在显著差异。本文从定义、语义、实现细节、典型用法以及最佳实践四个维度进行比较,并给出实际编程中的决策建议。

1. 基本定义与语义

特性 std::unique_ptr std::shared_ptr
所有权 只能有一个所有者 共享所有权,多个指针可指向同一对象
复制 禁止复制,支持移动 支持复制,内部维护引用计数
内存释放 立即销毁对象 当引用计数归零时销毁
适用场景 资源必须唯一拥有 需要多处引用,生命周期难以确定
  • std::unique_ptr:实现了独占式所有权。通过 std::move 可以转移所有权,但不能复制。适合资源必须被唯一拥有的情况,如文件句柄、网络连接、单例模式等。
  • std::shared_ptr:实现了共享式所有权。每一次拷贝都会递增引用计数,拷贝销毁时递减。适合资源需要在多处共享或存在循环引用的场景。

2. 典型实现细节

2.1 unique_ptr

template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
public:
    explicit unique_ptr(T* ptr = nullptr) noexcept : ptr_(ptr) {}
    ~unique_ptr() { if (ptr_) deleter_(ptr_); }

    unique_ptr(const unique_ptr&) = delete;           // 禁止拷贝
    unique_ptr& operator=(const unique_ptr&) = delete;

    unique_ptr(unique_ptr&& other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; }
    unique_ptr& operator=(unique_ptr&& other) noexcept {
        reset(other.release());
        return *this;
    }

    T* get() const noexcept { return ptr_; }
    T& operator*() const noexcept { return *ptr_; }
    T* operator->() const noexcept { return ptr_; }

    void reset(T* ptr = nullptr) noexcept {
        if (ptr_ != ptr) { deleter_(ptr_); ptr_ = ptr; }
    }

    T* release() noexcept { T* tmp = ptr_; ptr_ = nullptr; return tmp; }

private:
    T* ptr_;
    Deleter deleter_;
};

关键点:

  • 禁止拷贝构造和拷贝赋值。
  • 支持移动构造和移动赋值。
  • 内部没有引用计数,因而开销极小。

2.2 shared_ptr

template <typename T>
class shared_ptr {
public:
    explicit shared_ptr(T* ptr = nullptr) noexcept : ptr_(ptr), ref_count_(new size_t(1)) {}
    ~shared_ptr() { release(); }

    shared_ptr(const shared_ptr& other) noexcept : ptr_(other.ptr_), ref_count_(other.ref_count_) {
        if (ptr_) ++(*ref_count_);
    }

    shared_ptr& operator=(const shared_ptr& other) noexcept {
        if (this != &other) {
            release();
            ptr_ = other.ptr_;
            ref_count_ = other.ref_count_;
            if (ptr_) ++(*ref_count_);
        }
        return *this;
    }

    shared_ptr(shared_ptr&& other) noexcept : ptr_(other.ptr_), ref_count_(other.ref_count_) {
        other.ptr_ = nullptr;
        other.ref_count_ = nullptr;
    }

    // ...

private:
    void release() noexcept {
        if (ptr_ && --(*ref_count_) == 0) {
            delete ptr_;
            delete ref_count_;
        }
    }

    T* ptr_;
    size_t* ref_count_;
};

关键点:

  • 拷贝构造和拷贝赋值都会递增计数。
  • 需要维护引用计数对象,导致内存分配额外开销。
  • 引用计数实现必须是原子操作(线程安全),或者使用 std::atomic.

3. 性能对比

项目 unique_ptr shared_ptr
复制成本 O(1),无计数操作 O(1),但需原子计数递增/递减
内存占用 仅指针 指针 + 计数指针 + 计数值
线程安全 不需要 需要原子计数(C++11 之后默认实现)
销毁开销 仅析构 计数递减 + 可能析构 + 计数器析构

在大多数情况下,unique_ptr 的性能要明显优于 shared_ptr。当资源所有权不需要共享时,应首选 unique_ptr

4. 典型用例

4.1 unique_ptr 用例

std::unique_ptr <File> file(new File("data.txt"));
auto process = [file = std::move(file)](const std::string& line) {
    file->write(line);
};
  • File 对象只能由 process 处理,所有权通过 std::move 转移。
  • 代码简洁,且避免了手动 delete

4.2 shared_ptr 用例

struct Node {
    std::vector<std::shared_ptr<Node>> children;
    std::weak_ptr <Node> parent;
};

std::shared_ptr <Node> root = std::make_shared<Node>();
auto child = std::make_shared <Node>();
child->parent = root;
root->children.push_back(child);
  • 父子节点共享同一内存块。
  • 为防止循环引用使用 std::weak_ptr

5. 何时选择?

场景 推荐指针
需要唯一所有权且无共享 std::unique_ptr
需要共享所有权,生命周期难以确定 std::shared_ptr
需要跨线程共享且引用计数线程安全 std::shared_ptr(配合 std::atomic)
需要自定义删除器 两者均可;unique_ptr 对单一删除器更简洁

小结unique_ptr 是最轻量级、最安全的智能指针,适合大多数需要 RAII 的情况;shared_ptr 在需要共享所有权时才使用,注意避免循环引用。掌握两者的语义差异和使用场景,可让 C++ 程序更健壮、更高效。


发表评论