unique_ptr

在任何时刻,只有一个unique_ptr可以拥有所管理的对象,不能多个

机制

  • 禁止拷贝构造和拷贝赋值

  • 支持移动语义

  • 轻量级

    unique_ptr在内存上非常轻量,其大小通常与一个裸指针相同,因为它不需要维护引用计数等额外数据

  • 支持自定义删除器

    这使得它不仅可以管理堆内存,还可以管理其他类型的资源(如文件句柄、互斥锁等),并在资源释放时执行特定的清理操作

    void file_deleter(FILE* fp) {
      if (fp) {
          fclose(fp);
          std::cout << "File closed by custom deleter." << std::endl;
      }
    }
    
    void test_unique_ptr_custom_deleter() {
      // unique_ptr<FILE, decltype(&file_deleter)>
      std::unique_ptr<FILE, void(*)(FILE*)> file_ptr(fopen("test.txt", "w"), file_deleter);
      if (file_ptr) {
          fprintf(file_ptr.get(), "Hello from unique_ptr!");
      }
      // file_ptr 离开作用域时,file_deleter 会被调用
    }
    
  • 支持数组

  • 单次内存分配

  • 异常安全

    make_unique将对象的创建封装在一个原子操作中,避免了在表达式求值过程中因异常而导致的内存泄漏

shared_ptr

多个shared_ptr实例可以共同拥有并管理同一个动态分配的对象。当指向该对象的最后一个shared_ptr被销毁或重置时,该对象才会被自动析构并释放其占用的内存

机制

  • 每个shared_ptr对象都与一个内部的计数器关联,这个计数器记录了当前有多少个shared_ptr实例正在管理同一个对象。这个计数器通常被称为“强引用计数

  • 构造(增加)

    当一个新的shared_ptr被创建并指向一个对象时,强引用计数会增加1

  • 赋值(增减)

    当一个shared_ptr被赋值给另一个shared_ptr时,被赋值的shared_ptr会放弃它原来管理的对象,导致其原对象的强引用计数减少1。然后,它会开始管理新对象,导致新对象的强引用计数增加1

  • 析构/重置(减少)

    当一个shared_ptr实例被销毁或被显式地重置时,它所管理的对象的强引用计数会减少1

  • 资源释放

    当强引用计数减少到0时,shared_ptr会自动调用被管理对象的析构函数,并释放其占用的内存

  • 单次内存分配

    make_shared只进行一次内存分配,同时为对象和控制块分配内存,减少了内存碎片,提高了缓存局部性

  • 异常安全

    make_shared将对象的创建和shared_ptr的构造封装在一个原子操作中,避免了在表达式求值过程中因异常而导致的内存泄漏

控制块

shared_ptr之所以能够实现如此精巧的引用计数和资源管理,离不开其背后的核心组件——控制块

一个std::shared_ptr对象在内存中通常由两部分组成:

  1. 原始指针:指向它所管理的实际对象
  2. 控制块指针:指向一个独立于被管理对象的内存区域,存储与该对象生命周期管理相关的信息。所有共享同一个对象的shared_ptr实例都指向同一个控制块

控制块通常包含以下信息:

  • 强引用计数:记录有多少个std::shared_ptr实例正在管理该对象。当此计数归零时,被管理对象将被析构并释放

  • 弱引用计数:记录有多少个std::weak_ptr实例正在观察该对象

    即使强引用计数归零,只要弱引用计数不为零,控制块就不会被销毁

  • 原始指针

  • 自定义删除器

  • 自定义分配器

生命周期:控制块只有在强引用计数和弱引用计数都归零时才会被销毁

简化版shared_ptr

template <typename T>
class MySharedPtr {
public:
    // 默认构造函数
    MySharedPtr() : m_ptr(nullptr), m_ref_count(newint(0)) {
        std::cout << "Default constructor called. Ref count: " << *m_ref_count << std::endl;
    }

    // 接受裸指针的构造函数
    explicit MySharedPtr(T* ptr) : m_ptr(ptr), m_ref_count(new int(1)) {
        std::cout << "Constructor with raw pointer called. Ref count: " << *m_ref_count << std::endl;
    }

    // 拷贝构造函数
    MySharedPtr(const MySharedPtr& other) : m_ptr(other.m_ptr), m_ref_count(other.m_ref_count) {
        if (m_ref_count) {
            (*m_ref_count)++;
        }
        std::cout << "Copy constructor called. Ref count: " << *m_ref_count << std::endl;
    }

    // 赋值运算符
    MySharedPtr& operator=(const MySharedPtr& other) {
        if (this == &other) { // 处理自赋值
            return *this;
        }

        // 减少旧对象的引用计数
        if (m_ref_count) {
            (*m_ref_count)--;
            if (*m_ref_count == 0) {
                delete m_ptr;
                delete m_ref_count;
                std::cout << "Old object and ref count deleted in assignment." << std::endl;
            }
        }

        // 拷贝新对象的信息并增加引用计数
        m_ptr = other.m_ptr;
        m_ref_count = other.m_ref_count;
        if (m_ref_count) {
            (*m_ref_count)++;
        }
        std::cout << "Assignment operator called. Ref count: " << *m_ref_count << std::endl;
        return *this;
    }

    // 析构函数
    ~MySharedPtr() {
        if (m_ref_count) {
            (*m_ref_count)--;
            std::cout << "Destructor called. Current ref count: " << *m_ref_count << std::endl;
            if (*m_ref_count == 0) {
                delete m_ptr; // 释放被管理对象
                delete m_ref_count; // 释放引用计数器
                m_ptr = nullptr;
                m_ref_count = nullptr;
                std::cout << "Object and ref count deleted." << std::endl;
            }
        }
    }

    // 解引用运算符
    T& operator*() const {
        return *m_ptr;
    }

    // 箭头运算符
    T* operator->() const {
        return m_ptr;
    }

    // 获取引用计数
    int use_count() const {
        return m_ref_count ? *m_ref_count : 0;
    }

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

private:
    T* m_ptr;         // 指向被管理对象的指针
    int* m_ref_count; // 指向引用计数器的指针
};

weak_ptr

weak_ptr指向一个由shared_ptr管理的对象,但不会增加对象的强引用计数

机制

  • 不增加强引用计数

    只是“观察”对象,而不是“拥有”对象

  • 支持检查对象是否存活

    通过lock()方法来实现这一点。lock()会尝试返回一个shared_ptr。如果对象仍然存活,lock()会返回一个有效的shared_ptr,并增加对象的强引用计数;如果对象已经销毁,lock()会返回一个空的shared_ptr

  • 不能直接访问对象

    必须通过lock()方法将其转换为shared_ptr才能访问

循环引用

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    A() { std::cout << "A constructor" << std::endl; }
    ~A() { std::cout << "A destructor" << std::endl; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    B() { std::cout << "B constructor" << std::endl; }
    ~B() { std::cout << "B destructor" << std::endl; }
};

void test_circular_reference() {
    std::shared_ptr<A> pa = std::make_shared<A>();
    std::shared_ptr<B> pb = std::make_shared<B>();

    pa->b_ptr = pb; // A 拥有 B
    pb->a_ptr = pa; // B 拥有 A

    std::cout << "pa use_count: " << pa.use_count() << std::endl; // 输出 2
    std::cout << "pb use_count: " << pb.use_count() << std::endl; // 输出 2

    // 当 pa 和 pb 离开作用域时,它们的强引用计数会减到 1,但永远不会归零
    // A 和 B 的析构函数都不会被调用,导致内存泄漏
}

解决方案:使用weak_ptr打破循环。将循环引用中的一方改为weak_ptr,可以打破这种循环

class B_fixed;

class A_fixed {
public:
    std::shared_ptr<B_fixed> b_ptr;
    A_fixed() { std::cout << "A_fixed constructor" << std::endl; }
    ~A_fixed() { std::cout << "A_fixed destructor" << std::endl; }
};

class B_fixed {
public:
    std::weak_ptr<A_fixed> a_ptr; // 将强引用改为弱引用
    B_fixed() { std::cout << "B_fixed constructor" << std::endl; }
    ~B_fixed() { std::cout << "B_fixed destructor" << std::endl; }
};

void test_circular_reference_fixed() {
    std::shared_ptr<A_fixed> pa = std::make_shared<A_fixed>();
    std::shared_ptr<B_fixed> pb = std::make_shared<B_fixed>();

    pa->b_ptr = pb; // A_fixed 拥有 B_fixed
    pb->a_ptr = pa; // B_fixed 观察 A_fixed,不增加强引用计数

    std::cout << "pa use_count: " << pa.use_count() << std::endl; // 输出 1
    std::cout << "pb use_count: " << pb.use_count() << std::endl; // 输出 1

    // 当 pa 离开作用域时,A_fixed 对象的强引用计数变为 0,A_fixed 被销毁
    // 此时 pb->a_ptr 会失效
    // 当 pb 离开作用域时,B_fixed 对象的强引用计数变为 0,B_fixed 被销毁
    // 内存得到正确释放
}

总结

  • 优先使用unique_ptr

  • 当需要共享所有权时使用shared_ptr

  • 警惕并解决shared_ptr的循环引用

  • 优先使用std::make_uniquestd::make_shared

    不仅提供异常安全,还能减少内存分配次数

  • 避免裸指针与智能指针混用

  • 利用自定义删除器管理非内存资源

  • shared_ptr管理的对象本身不是线程安全的

    shared_ptr的引用计数(内存管理)是线程安全的,但其所管理的对象的数据成员的访问并不是

    • 多个线程可以同时拷贝、赋值、销毁同一个 shared_ptr 对象,其内部的引用计数会通过原子操作正确维护
    • 当最后一个 shared_ptr 实例被销毁时,它所管理的资源(即原始对象)会被安全地释放,这个过程也是线程安全的
    • shared_ptr 仅仅保证了它对内存的管理是线程安全的,但它不保证多个线程同时访问或修改其内部指向的原始对象时不会发生数据竞争
    • 如果原始对象的数据成员在多线程环境下被并发修改,仍需自行添加同步机制(如 std::mutex)来保护对原始对象的访问

他们曾如此骄傲的活过,贯彻始终