C++98中的auto_ptr存在诸多问题,未被广泛使用。在C++11标准库中真正引入了智能指针,包括unique_ptrshared_ptrweak_ptr,智能指针的设计初衷就是为了帮助开发者管理内存。

unique_ptr

std::unique_ptr<T>std::shared_ptr<T>具有更小的内存,而且不需要维护引用计数,因此它的性能更好。当我们需要一个独占的指针时,应该优先使用unique_ptr

特性

字面意思,unique_ptr最大的特性就是独占所有权,即同一时间只能有一个 unique_ptr 拥有某个对象的所有权。它能够自动管理内存并在不再使用时释放资源,从而避免内存泄漏。

  1. 独占所有权:独占所指对象的所有权,不能共享;
  2. 不能复制:禁止拷贝,确保了资源的唯一所有权;
  3. 自动销毁:当 unique_ptr 离开其作用域时(如函数结束或对象被销毁),它会自动释放所指向的资源,不需要显式调用 delete
  4. 轻量高效:相比于 shared_ptrunique_ptr 没有额外的引用计数开销。

特性实现

独占所有权

禁止拷贝

    // 删除了拷贝构造函数,确保同一时间只有一个 MyUniquePtr 对象拥有资源的所有权,防止拷贝操作
    MyUniquePtr(const MyUniquePtr& other) = delete;
    
    // 删除了拷贝赋值操作符,确保不会通过赋值创建多个 MyUniquePtr 对象同时管理同一资源
    MyUniquePtr& operator=(const MyUniquePtr& other) = delete;

移动语义

    // 移动构造函数:转移所有权
    MyUniquePtr(MyUniquePtr&& other) noexcept : ptr(other.ptr) {
        other.ptr = nullptr;  // 将源指针置空,确保只有一个指针管理资源
    }

    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
        if (this != &other) {  // 防止自我赋值
            delete ptr;         // 释放当前持有的资源
            ptr = other.ptr;    // 转移新资源的所有权
            other.ptr = nullptr; // 将源指针置空
        }
        return *this;
    }

自动销毁

析构释放

    // 析构函数:释放管理的资源
    ~MyUniquePtr() {
        delete ptr;  // 自动删除所管理的对象
    }

访问对象

    // 重载 * 操作符,方便访问对象
    T& operator*() const {
        return *ptr;
    }

    // 重载 -> 操作符,方便访问对象的成员
    T* operator->() const {
        return ptr;
    }

    // 获取原始指针
    T* get() const {
        return ptr;
    }

    // 放弃所有权,不删除对象,并返回指针
    T* release() {
        T* temp = ptr;
        ptr = nullptr;
        return temp;
    }

shared_ptr

通常用于一些资源创建昂贵比较耗时的场景, 比如涉及到文件读写、网络连接、数据库连接等。当需要共享资源的所有权时,例如,一个资源需要被多个对象共享,但是不知道哪个对象会最后释放它,这时候就可以使用std::shared_ptr<T>

特性

不同于unique,shared_ptr可以共享所有权,并引入了引用计数特性。

  1. 共享所有权:多个 shared_ptr 对象可以共享管理同一个资源;
  2. 引用计数:维护一个引用计数,每当有新的 shared_ptr 复制或移动该资源时,引用计数会增加;当某个 shared_ptr 被销毁时,引用计数减少。只有当引用计数为零时,资源才会被释放;
  3. 线程安全的引用计数shared_ptr 的引用计数操作是线程安全的,因此它可以安全地在多线程环境下使用;
  4. 联动weak_ptr:与 weak_ptr 协作,防止循环引用的问题。

特性实现

共享所有权

MySharedPtr 支持拷贝构造和赋值操作,通过增加引用计数实现共享所有权。

    // 拷贝构造函数,增加引用计数
    MySharedPtr(const MySharedPtr& other) : ptr(other.ptr), ref_count(other.ref_count) {
        (*ref_count)++;  // 引用计数增加
        std::cout << "Copied shared_ptr, ref_count = " << *ref_count << std::endl;
    }

引用计数

ref_count 用于跟踪有多少个 MySharedPtr 对象共享同一个资源。每当有新的 MySharedPtr 对象拷贝构造时,引用计数增加;当一个对象被销毁时,引用计数减少。

    // 构造函数
    explicit MySharedPtr(T* p = nullptr) : ptr(p), ref_count(new int(1)) {
        std::cout << "Created shared_ptr, ref_count = 1" << std::endl;
    }

    // 赋值运算符,处理引用计数
    MySharedPtr& operator=(const MySharedPtr& other) {
        if (this != &other) {
            // 先减少当前对象的引用计数
            release();
            // 复制新对象的指针和引用计数
            ptr = other.ptr;
            ref_count = other.ref_count;
            (*ref_count)++;  // 引用计数增加
            std::cout << "Assigned shared_ptr, ref_count = " << *ref_count << std::endl;
        }
        return *this;
    }

线程安全

shared_ptr 使用了原子操作来管理其引用计数。因此,当多个线程同时复制、销毁或重新赋值 shared_ptr 时,引用计数的增减操作是原子性的,不会发生竞态条件(race condition)。

weak_ptr

常用于数据结构中防止 shared_ptr 之间的循环依赖。例如在树结构、图结构或观察者模式中,经常使用 weak_ptr 来防止内存泄漏。当你不希望影响对象生命周期,但需要临时访问某个对象时,可以使用 weak_ptr

特性

  1. 不影响引用计数weak_ptr 不会增加 shared_ptr 的强引用计数。这意味着,即使有 weak_ptr 指向某个对象,当所有 shared_ptr 都销毁时,该对象仍然会被释放。
  2. 避免循环引用:在复杂的数据结构中,两个对象可能互相引用。如果双方都使用 shared_ptr,则会产生循环引用,导致内存泄漏。使用 weak_ptr 可以解决这个问题,因为 weak_ptr 不会阻止对象的销毁。
  3. 只能通过 lock() 获取对象:由于 weak_ptr 不直接拥有对象,它无法直接访问被引用的对象。需要调用 lock() 方法将其转换为 shared_ptr,这样可以确保在访问对象时对象仍然存在。

特性实现

不影响生命周期

weak_ptrshared_ptr 共享同一个控制块,这个控制块包含了两个计数器:

  • 强引用计数:跟踪有多少个 shared_ptr 实例引用同一个对象。
  • 弱引用计数:跟踪有多少个 weak_ptr 引用该对象。

控制块不仅存储对象的引用计数,还保存着对象的指针(对象的地址)。当 shared_ptr 引用的对象被销毁时,控制块不会立即被释放,因为 weak_ptr 可能还在使用它。

weak_ptr 不会增加对象的强引用计数,因此它不会影响对象的生命周期。即使有多个 weak_ptr 引用该对象,当所有的 shared_ptr 被销毁时,强引用计数为 0,资源会被释放。

这通过引用计数的拆分实现,weak_ptr 只影响弱引用计数,不会干涉 shared_ptr 的强引用计数。

weak_ptr() : ptr_(nullptr), ref_count_(nullptr), weak_count_(nullptr) {}

    weak_ptr(const shared_ptr<T>& sharedPtr) : ptr_(sharedPtr.ptr_), ref_count_(sharedPtr.ref_count_), weak_count_(sharedPtr.weak_count_) {
        if (weak_count_) (*weak_count_)++;
    }

避免循环引用

weak_ptr 最典型的用途是解决循环引用问题。在两个对象互相引用时,如果都使用 shared_ptr,对象将无法自动释放,因为它们的强引用计数永远不会降到 0。weak_ptr 通过不增加强引用计数来打破这个循环,使对象在强引用计数归零时能够被正确销毁。

访问对象

weak_ptr 没有直接访问对象的能力,需要通过 lock() 将自己转换为一个临时的 shared_ptr 来访问对象。

lock() 的实现原理是检查强引用计数是否大于 0。如果大于 0,表示对象还存在,于是 lock() 返回一个新的 shared_ptr,并增加强引用计数;如果强引用计数为 0,lock() 返回一个空的 shared_ptr

    shared_ptr<T> lock() const {
        // 检查强引用计数是否大于 0,如果大于 0,表示对象仍然存在
        if (ref_count_ && *ref_count_ > 0) {
            return shared_ptr<T>(*this);  // 创建并返回一个新的 shared_ptr
        }
        return shared_ptr<T>(nullptr);    // 否则返回一个空的 shared_ptr
    }

参考阅读