C++ —— 智能指针

1. 智能指针

1.1 智能指针的使用场景

来看以下场景:

cpp 复制代码
double Divide(int a, int b)
{
    // 当 b == 0时抛出异常
    if (b == 0)
    {
        throw "Divide by zero condition!";
    }
    else
    {
        return (double)a / (double)b;
    }
}

void Func()
{
    int* array1 = new int[10];
    int* array2 = new int[10];   // 抛异常呢
    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        cout << "delete [] " << array1 << endl;
        cout << "delete [] " << array2 << endl;
        delete[] array1;
        delete[] array2;
        throw;  // 异常重新抛出,捕获到什么抛出什么
    }

    cout << "delete [] " << array1 << endl;
    delete[] array1;
    cout << "delete [] " << array2 << endl;
    delete[] array2;
}

int main()
{
    try
    {
        Func();
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;
    }
    catch (...)
    {
        cout << "未知异常" << endl;
    }

    return 0;
}

如果上述代码发生除 0 错误抛出异常,另外下面的 array1 和 array2 没有得到释放。所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去。但是如果 array2 new 的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案是使用智能指针


1.2 RAII

RAII 是 Resource Acquisition Is Initialization(资源获取即初始化 )的缩写,它是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。

RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会像迭代器类一样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。

智能指针的基本设计,核心就是利用析构函数自动释放资源

示例代码:

cpp 复制代码
template <typename T>
class SmartPointer
{
public:
    SmartPointer(T* ptr) : m_ptr(ptr) {}

    ~SmartPointer()
    {
        cout << "delete [] " << m_ptr << endl;
        delete[] m_ptr;
    }

    T& operator*() { return *m_ptr; }
    T* operator->() { return m_ptr; }
    T& operator[](size_t index) { return m_ptr[index]; }

private:
    T* m_ptr;
};

之前申请的资源,都需要手动释放。有了智能指针,可以将资源交给智能指针来管理。

通过调用SmartPointer的析构函数的来释放资源。因此之后申请资源都通过智能指针来申请:SmartPointer<int> sp = new int[10],将 p 传递给智能指针,智能指针将负责释放内存。

通过智能指针可以解决开头出现的问题:

cpp 复制代码
double Divide(int a, int b)
{
    // 当 b == 0时抛出异常
    if (b == 0) { throw "Divide by zero condition!"; }
    else { return (double)a / (double)b; }
}

void Func()
{
    //int* array1 = new int[10];
    //int* array2 = new int[10];  // 抛异常呢
    SmartPointer<int> array1(new int[10]);
    SmartPointer<int> array2(new int[10]);

    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        throw; // 异常重新抛出,捕获到什么抛出什么
    }
}

使用智能指针来管理申请的资源。抛出异常,清理调用链上创建的对象,其中就包括 array1 和 array2,调用析构函数,释放自己。最终,才跳转到catch子句。无论是正常结束还是异常结束,都可以保障 new 的资源正常释放。

示例结果:


1.3 三种智能指针

智能指针核心的问题:所有权管理与拷贝语义

cpp 复制代码
SmartPointer<int> sp1 = new int[10];

SmartPointer<int> sp2 = new int[10];

SmartPointer<int> sp3(sp2);     // 错误:智能指针不允许复制构造,同一份资源会释放两次

这里sp2和sp3指向同一份资源,智能指针模拟的是指针的行为,托管资源,资源并不属于它。使用一个智能指针去拷贝另一个智能指针,期望的是两个指针指向同一份资源,但核心难点在于谁来负责释放、何时释放。如何解决智能指针的拷贝问题,这就牵扯到了智能指针的不同设计策略。

C++标准库中的智能指针都在**<memory>** 这个头文件下面,我们包含<memory>就可以使用了。智能指针有好几种,除了weak_ptr之外,其它的都符合RAII且支持像指针一样访问资源 ,weak_ptr同样符合RAII但不直接提供资源访问(没有operator*/operator->),它只是一个观察者,需要lock()提升为shared_ptr才能访问。原理上而言,不同的智能指针主要是解决所有权共享与拷贝时的策略不同


auto_ptr 是C++98时设计出来的智能指针,特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,原对象被置为null。这是一个非常糟糕的设计,因为它会将被拷贝对象悬空,访问报错的问题。强烈建议不要使用auto_ptr,其他C++11出来之前很多公司也是明令禁止使用这种智能指针的。

示例代码:

cpp 复制代码
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1);      // ap1 的所有权转移给 ap2,ap1 不再拥有 Date 对象

ap1->_year = 2024;            // 错误:ap1 不再拥有 Date 对象,访问 ap1 是未定义行为
ap2->_year = 2025;            // 正确:ap2 拥有 Date 对象,可以访问和修改它

报错显示:


unique_ptr是C++11设计出来的智能指针,它的名字翻译出来是唯一指针,它的特点的不支持拷贝(拷贝构造和拷贝赋值被delete) ,只支持移动(移动构造和移动赋值)。如果不需要拷贝的场景就非常建议使用它。

示例代码:

cpp 复制代码
unique_ptr<Date> up1(new Date);

unique_ptr<Date> up2(up1);        // 错误:unique_ptr 不允许拷贝
unique_ptr<Date> up2 = up1;       // 错误:unique_ptr 不允许拷贝

unique_ptr<Date> up3 = move(up1); // 将 up1 的所有权转移给 up3,up1 不再拥有 Date 对象

// up1->_year = 2024;             // 错误:up1 不再拥有 Date 对象,访问 up1 是未定义行为
up3->_year = 2025;                // 正确:up3 拥有 Date 对象,可以访问和修改它

示例结果:


shared_ptr 是C++11设计出来的智能指针,它的名字翻译出来是共享指针,它的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用它了。shared_ptr 内部通过引用计数管理资源,引用计数的增减是原子操作(线程安全),但shared_ptr管理的对象本身不是线程安全的。

示例代码:

cpp 复制代码
unique_ptr<Date> up1(new Date);

unique_ptr<Date> up2(up1);        // 错误:unique_ptr 不允许拷贝
unique_ptr<Date> up2 = up1;       // 错误:unique_ptr 不允许拷贝

unique_ptr<Date> up3 = move(up1); // 将 up1 的所有权转移给 up3,up1 不再拥有 Date 对象

// up1->_year = 2024;             // 错误:up1 不再拥有 Date 对象,访问 up1 是未定义行为
up3->_year = 2025;                // 正确:up3 拥有 Date 对象,可以访问和修改它

sp1,sp2,sp3都指向同一份资源,但是却只会析构一次:

这是因为智能指针的底层是用引用计数的方式实现的。当计数器为0时,才调用析构函数释放指向的资源。计数器的变量名为 use_count。

move 会导致 sp1 的资源被转移,导致sp1指针悬空。


auto_ptr,unique_ptr,shared_ptr 这三个智能指针都重载了 operator->,operator*,operator bool(检查指针是否为空)。它俩支持了operator bool ,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空

shared_ptr 实现了 reset 接口,功能为:手动释放指向资源的shared_ptr指针。

示例代码:

cpp 复制代码
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);      // sp1 和 sp2 共享 Date 对象的所有权
shared_ptr<Date> sp3 = sp1;      // sp1 和 sp3 也共享 Date 对象的所有权

if (sp1)                         // 检查 sp1 是否为空
{
    cout << "sp1.use_count() = " << sp1.use_count() << endl;
    sp1.reset();                 // sp1 释放对 Date 对象的所有权,sp2 和 sp3 仍然拥有 Date 对象
    cout << "sp1.use_count() = " << sp1.use_count() << endl;
}

if (sp2)                         // 检查 sp2 是否为空
{
    cout << "sp2.use_count() = " << sp2.use_count() << endl;
    sp2.reset();                 // sp2 释放对 Date 对象的所有权,sp3 仍然拥有 Date 对象
    cout << "sp2.use_count() = " << sp2.use_count() << endl;
}

if (sp3)                         // 检查 sp3 是否为空
{
    cout << "sp3.use_count() = " << sp3.use_count() << endl;
    sp3.reset();                 // sp3 释放对 Date 对象的所有权,Date 对象被销毁
    cout << "sp3.use_count() = " << sp3.use_count() << endl;
}

示例结果:

get 接口 ------ 功能:获取底层的指针。

示例代码:

cpp 复制代码
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);         // sp1 和 sp2 共享 Date 对象的所有权
shared_ptr<Date> sp3 = sp1;        // sp1 和 sp3 也共享 Date 对象的所有权

cout << sp1.get() << endl;         // 获取 sp1 底层的指针,输出 Date 对象的地址
cout << sp2.get() << endl;         // 获取 sp2 底层的指针,输出 Date 对象的地址,与 sp1 相同
cout << sp3.get() << endl;         // 获取 sp3 底层的指针,输出 Date 对象的地址,与 sp1 相同

示例结果:

shared_ptr 中有一种构造函数:别名构造函数。

cpp 复制代码
int* p = new int(10);
shared_ptr<int> a(new int(20));
shared_ptr<int> b(a, p);    // b 共享 a 的所有权,但管理一个不同的指针 p

只是存储指针为 p。b 不拥有 p,也不会管理其存储。相反,它共同拥有 a 的托管对象,并将其视为 a 的一个额外用途。

可以打印看看,a,b,p三者的地址:

b 的存储指针 (get()返回)是p的地址,但是b和a共享同一个控制块和引用计数,b通过别名指针p来访问资源。

可以看看a,b的引用计数:由此也可以看出 a,b 指向的是同一份资源。


weak_ptr 是 C++11 设计出来的智能指针,它的名字翻译出来是弱指针,它完全不同于上面的智能指针,它不拥有目标资源的所有权 (不增加引用计数),也就意味着不能直接用它来管理资源的释放。weak_ptr的产生本质是要解决 shared_ptr 的一个循环引用导致内存泄漏的问题。weak_ptr本身符合RAII设计,其构造和析构会正确管理控制块中的弱引用计数


智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。

智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。

因为new[ ]经常使用,所以为了简洁一点,unique_ptr和shared_ptr都特化了一份[ ]的版本,使用时 unique_ptr<Date[ ]> up1(new Date[5]),shared_ptr<Date[ ]> sp1(new Date[5]) 就可以管理new[ ]的资源。

示例:


资源不一定是new出来的,在早期不支持上述的方式时,是通过定制删除器来解决的。在构造智能指针时,作为参数传递。

D del 就是删除器。

shared_ptr 的删除器是在构造时传递:

cpp 复制代码
shared_ptr<Date> sp2(new Date[10], DeleteArray<Date>());  // 使用 DeleteArray 作为自定义删除器

shared_ptr<Date> sp3(new Date[10], [](Date* p) {    // 使用 lambda 表达式作为自定义删除器
    cout << "delete [] " << p << endl;
    delete[] p;
});

unique_ptr 的删除器是在模板参数处指定类型,构造函数处可以传入具体的删除器对象:

cpp 复制代码
unique_ptr<Date[], DeleteArray<Date>> up2(new Date[10]);   // unique_ptr 需要指定删除器类型
unique_ptr<Date[], void(*)(Date*)> up3(new Date[10], [](Date* p) {
    cout << "delete [] " << p << endl;
    delete[] p;
});    // unique_ptr 需要指定删除器类型

unique_ptr<Date, DeleteArray<Date>> up2(new Date[10]);    // unique_ptr 需要指定删除器类型

unique_ptr<Date, void(*)(Date*)> up3(new Date[10], [](Date* p) {
    cout << "delete [] " << p << endl;
    delete[] p;
});    // unique_ptr 需要指定删除器类型

shared_ptr 除了支持用指向资源的指针构造,还支持直接构造。make_shared 与 make_pair 有些类似,但是它与make_pair 有所不同,make_pair 的原型:template <class T1, class T2> pair<T1,T2> make_pair (T1 x, T2 y)。使用模板构造一个对象。make_shared 的原型:template <class T, class... Args> shared_ptr<T> make_shared (Args&&... args)。将参数 args 传递给其构造函数,并返回一个 shared_ptr<T> 类型的对象,该对象永远并存储指向它的指针。make_shared 的类型需要显式指定模板参数

cpp 复制代码
shared_ptr<Date> sp1(new Date(2026, 4, 17));
// 使用 make_shared 创建 shared_ptr, 效率更高, 代码更简洁
shared_ptr<Date> sp2 = make_shared<Date>(2026, 4, 17);
auto sp3 = make_shared<Date>(2026, 4, 17); // 使用 auto 自动推导类型, 代码更简洁

在后期的标准中增加了 make_unique,在C++11中还不存在。

shared_ptr 和 unique_ptr 的构造函数都使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。

cpp 复制代码
// shared_ptr<Date> sp1 = new Date; // 不支持隐式转换,编译错误: 无法将 Date* 转为 shared_ptr<Date>
shared_ptr<Date> sp2(new Date);

总结:shared_ptr 和 unique_ptr 的区别

特性 unique_ptr shared_ptr
所有权语义 独占所有权 共享所有权
拷贝语义 禁止拷贝,只允许移动 支持拷贝(引用计数+1),支持移动
内存开销 一个指针(零开销) 两个指针 + 控制块
性能 无额外开销 引用计数原子操作有开销
删除器 模板参数,编译期确定 构造函数参数,运行时类型擦除
使用场景 明确资源唯一归属,默认首选 需要资源共享,注意循环引用
相互转换 可隐式转为shared_ptr 不可转为unique_ptr

1.4 智能指针的原理

auto_ptr 的核心原理:拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。

模拟实现 auto_ptr:

cpp 复制代码
template<class T>
class auto_ptr
{
public:
    auto_ptr(T* ptr = nullptr) : m_ptr(ptr) {}       // 构造函数,接受一个原始指针
    ~auto_ptr() { delete m_ptr; }                   // 析构函数,释放资源
    auto_ptr(auto_ptr& other) : m_ptr(other.m_ptr) { other.m_ptr = nullptr; }   // 拷贝构造函数,转移所有权
    auto_ptr& operator=(auto_ptr& other)           // 赋值运算符,转移所有权
    {
        if (this != &other)
        {
            if(m_ptr) delete m_ptr; // 释放当前对象拥有的资源
            m_ptr = other.m_ptr;    // 转移所有权
            other.m_ptr = nullptr;  // 将其他对象的指针置空,防止重复删除
        }
        return *this;
    }
    T& operator*() { return *m_ptr; }
    T* operator->() { return m_ptr; }

private:
    T* m_ptr;
};

unique_ptr 的核心原理:不支持拷贝

模拟实现 unique_ptr:

cpp 复制代码
template <class T>
class unique_ptr
{
public:
    unique_ptr(T* ptr = nullptr) : m_ptr(ptr) {}         // 构造函数,接受一个原始指针
    ~unique_ptr() { delete m_ptr; }                     // 析构函数,释放资源
    unique_ptr(const unique_ptr&) = delete;             // 禁止拷贝构造
    unique_ptr& operator=(const unique_ptr&) = delete;   // 禁止赋值运算符
    // 移动构造函数,转移所有权
    unique_ptr(unique_ptr&& other) noexcept : m_ptr(other.m_ptr) { other.m_ptr = nullptr; }
    unique_ptr& operator=(unique_ptr&& other) noexcept  // 移动赋值运算符,转移所有权
    {
        if (this != &other)
        {
            if (m_ptr) delete m_ptr;  // 释放当前对象拥有的资源, 前提是当前对象必须拥有资源
            m_ptr = other.m_ptr;      // 转移所有权
            other.m_ptr = nullptr;     // 将其他对象的指针置空,防止重复删除
        }
        return *this;
    }
    T& operator*() { return *m_ptr; }
    T* operator->() { return m_ptr; }

private:
    T* m_ptr;
};

C++98不支持拷贝和赋值重载的方式为:只声明,不实现,声明成私有。


shared_ptr 需要重点学习,它是如何设计的,尤其是引用计数的设计。

主要这里一份资源就需要一个引用计数,所以引用计数采用静态成员的方式是无法实现的,要使用堆上动态开辟的方式。每构造一个智能指针对象就需要来一份资源,就要new一个引用计数出来。

若是采用 static 静态成员变量,所有创建出来的智能指针对象使用的都是同一个计数器,这表明着这些的智能指针指向的都是同一份资源。多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。

模拟实现 shared_ptr:

cpp 复制代码
namespace AY
{
    template <class T>
    class shared_ptr
    {
    public:
        // 构造函数,接受一个原始指针,并初始化引用计数为 1
        shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_count(new int(1)) {}

        void release()  // 释放资源,减少引用计数,如果引用计数为 0 则删除资源
        {
            if (--(*m_count) == 0) // 采用前置--,先递减引用计数,再检查是否为 0
            {
                cout << "delete " << m_ptr << endl;
                delete m_ptr;    // 删除资源,前提是 new 出来的对象
                delete m_count;  // 删除引用计数,前提是 new 出来的对象
            }
        }

        // 析构函数,释放资源,如果引用计数为 0 则删除资源
        ~shared_ptr()
        {
            release();  // 调用 release 函数来释放资源
        }

        // 拷贝构造函数,增加引用计数
        shared_ptr(const shared_ptr<T>& other) : m_ptr(other.m_ptr), m_count(other.m_count)
        {
            ++(*m_count);  // 增加引用计数
        }

        // 赋值运算符,先释放当前资源,再增加引用计数
        shared_ptr<T>& operator=(const shared_ptr<T>& other)
        {
            // 自赋值检查:sp1 = sp1, sp1 = sp2
            if (m_ptr != other.m_ptr)        // 检查自赋值
            {
                release();                   // 判断是否需要释放当前资源
                m_ptr = other.m_ptr;          // 复制指针和引用计数
                m_count = other.m_count;
                ++(*m_count);                // 增加引用计数
            }
            return *this;
        }

        T& operator*()  { return *m_ptr; }
        T* operator->() { return m_ptr; }
        T& operator[](size_t index) { return m_ptr[index]; }
        int use_count() const { return *m_count; }  // 获取引用计数

    private:
        T* m_ptr;      // 原始指针,指向共享的资源
        int* m_count;  // 引用计数,记录有多少个 shared_ptr 实例共享同一个资源
    };
}

使用模拟实现的 shared_ptr。

示例代码:

cpp 复制代码
AY::shared_ptr<Date> sp1(new Date(2026, 4, 17));
AY::shared_ptr<Date> sp2(sp1);                      // sp1 和 sp2 共享 Date 对象的所有权
cout << "sp1.use_count() = " << sp1.use_count() << endl;   // 输出 2,sp1 和 sp2 共享 Date 对象的所有权
cout << "sp2.use_count() = " << sp2.use_count() << endl;   // 输出 2,sp1 和 sp2 共享 Date 对象的所有权

AY::shared_ptr<Date> sp3(new Date);                 // sp3 拥有一个新的 Date 对象,与 sp1 和 sp2 不共享
sp1 = sp3;    // sp1 释放对原来 Date 对象的所有权,增加对 sp3 的 Date 对象的所有权,sp2 仍然拥有原来 Date 对象的所有权
cout << "sp1.use_count() = " << sp1.use_count() << endl;   // 输出 2,sp1 和 sp3 共享 Date 对象的所有权
cout << "sp2.use_count() = " << sp2.use_count() << endl;   // 输出 1,sp2 仍然拥有原来 Date 对象的所有权
cout << "sp3.use_count() = " << sp3.use_count() << endl;   // 输出 2,sp1 和 sp3 共享 Date 对象的所有权

示例结果:

实现定制删除器版本的 shared_ptr:

cpp 复制代码
namespace AY
{
    template <class T>
    class shared_ptr
    {
    public:
        // 构造函数,接受一个原始指针,并初始化引用计数为 1
        shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_count(new int(1)) {}

        // 定制删除的构造函数,使用一个成员来保存删除器
        template <class D>
        shared_ptr(T* ptr, D del) : m_ptr(ptr), m_count(new int(1)), m_del(del) {}

        void release()  // 释放资源,减少引用计数,如果引用计数为 0 则删除资源
        {
            if (--(*m_count) == 0) // 采用前置--,先递减引用计数,再检查是否为 0
            {
                cout << "delete " << m_ptr << endl;
                m_del(m_ptr);      // 释放资源时,使用删除器去释放,调用operator()
                delete m_count;
            }
        }

        // 析构函数,释放资源,如果引用计数为 0 则删除资源
        ~shared_ptr()
        {
            release();  // 调用 release 函数来释放资源
        }

        // 拷贝构造函数,增加引用计数
        shared_ptr(const shared_ptr<T>& other)
            : m_ptr(other.m_ptr), m_count(other.m_count), m_del(other.m_del)
        {
            ++(*m_count);  // 增加引用计数
        }

        // 赋值运算符,先释放当前资源,再增加引用计数
        shared_ptr<T>& operator=(const shared_ptr<T>& other)
        {
            // 自赋值检查:sp1 = sp1, sp1 = sp2
            if (m_ptr != other.m_ptr)        // 检查自赋值
            {
                release();                   // 判断是否需要释放当前资源
                m_ptr = other.m_ptr;          // 复制指针和引用计数
                m_count = other.m_count;
                ++(*m_count);                 // 增加引用计数
                m_del = other.m_del;          // 复制删除器
            }
            return *this;
        }

        T& operator*() { return *m_ptr; }
        T* operator->() { return m_ptr; }
        T& operator[](size_t index) { return m_ptr[index]; }
        int use_count() const { return *m_count; }  // 获取引用计数

    private:
        T* m_ptr;              // 原始指针,指向共享的资源
        int* m_count;          // 引用计数,记录有多少个 shared_ptr 实例共享同一个资源
        // 为了能够在这里也使用上模板参数 D,可以定义在类模板中
        // 或者直接使用 std::function<void(T*)> 来保存删除器,这样就不需要模板参数 D 了
        // 默认删除器,使用 lambda 表达式来定义一个默认的删除器,调用 delete 来释放资源
        function<void(T*)> m_del = [](T* ptr) { delete ptr; };
    };
}

示例代码及其结果:


shared_ptr 的循环引用的问题

shared_ptr 大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用 weak_ptr 解决这种问题。weak_ptr 就是用来专门解决 shared_ptr 的循环引用问题

循环引用场景:

cpp 复制代码
struct ListNode
{
    int _data;
    std::shared_ptr<ListNode> _next;
    std::shared_ptr<ListNode> _prev;

    ListNode(int data = 0) : _data(data), _next(nullptr), _prev(nullptr) {}
    ~ListNode() { cout << "~ListNode()" << endl; }
};

int main()
{
    /*ListNode* node1 = new ListNode{ 1, nullptr, nullptr };
    ListNode* node2 = new ListNode{ 2, nullptr, nullptr };*/

    // 既然如此,我们可以使用 shared_ptr 来管理 ListNode 对象的生命周期,避免内存泄漏
    std::shared_ptr<ListNode> node1(new ListNode{ 1 });
    std::shared_ptr<ListNode> node2(new ListNode{ 2 });

    // 但是,如果我们让 node1 和 node2 互相持有对方的 shared_ptr,就会导致循环引用,最终导致内存泄漏
    node1->_next = node2;  // node1 持有 node2 的原始指针
    node2->_prev = node1;  // node2 持有 node1 的原始指针

    return 0;
}

分析出现循环引用的原因,如下图所示:

n1和n2析构后,管理两个节点的引用计数减到1,

  1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了

  2. _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。

  3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。

  4. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了

这就构成了循环引用,_next 和 _prev 释放前提都是对方释放,导致内存泄漏。

解决方案:把 ListNode 结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用。

改进后的代码:

cpp 复制代码
int* p = new int(10);
shared_ptr<int> a(new int(10));
shared_ptr<int> b(a, p);  // a 和 b 共享 int 对象的所有权,但 b 使用 p 作为底层指针

测试代码及其结果:


shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原⼦操作保证线程安全的。

shared_ptr 智能指针的引用计数操作是线程安全的 (原子操作),但shared_ptr对象本身的并发读写(多个线程同时操作同一个shared_ptr对象)需要外部同步。它所指向的资源不是线程安全的,这个对象的线程安全问题不归shared_ptr管,应该由外层使用 shared_ptr 的人进行线程安全的控制。

shared_ptr引用计数从 int* 改成 atomic* 就可以保证引用计数的线程安全问题 ,或者使用互斥锁加锁也可以。它是C++库中的。

改进后,模拟实现的 shared_ptr 如下所示:

cpp 复制代码
template <class T>
class shared_ptr
{
public:
    // 构造函数,接受一个原始指针,并初始化引用计数为 1
    shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_count(new atomic<int>(1)) {}

    // 定制删除的构造函数,使用一个成员来保存删除器
    template <class D>
    shared_ptr(T* ptr, D del) : m_ptr(ptr), m_count(new atomic<int>(1)), m_del(del) {}

    void release()  // 释放资源,减少引用计数,如果引用计数为 0 则删除资源
    {
        if (--(*m_count) == 0) // 采用前置--,先递减引用计数,再检查是否为 0
        {
            cout << "delete " << m_ptr << endl;
            m_del(m_ptr);      // 释放资源时,使用删除器去释放,调用operator()
            delete m_count;
        }
    }

    // 析构函数,释放资源,如果引用计数为 0 则删除资源
    ~shared_ptr()
    {
        release();  // 调用 release 函数来释放资源
    }

    // 拷贝构造函数,增加引用计数
    shared_ptr(const shared_ptr<T>& other)
        : m_ptr(other.m_ptr), m_count(other.m_count), m_del(other.m_del)
    {
        ++(*m_count);  // 增加引用计数
    }

    // 赋值运算符,先释放当前资源,再增加引用计数
    shared_ptr<T>& operator=(const shared_ptr<T>& other)
    {
        // 自赋值检查:sp1 = sp1, sp1 = sp2
        if (m_ptr != other.m_ptr)        // 检查自赋值
        {
            release();                   // 判断是否需要释放当前资源
            m_ptr = other.m_ptr;          // 复制指针和引用计数
            m_count = other.m_count;
            ++(*m_count);                 // 增加引用计数
            m_del = other.m_del;          // 复制删除器
        }
        return *this;
    }

    T& operator*() { return *m_ptr; }
    T* operator->() { return m_ptr; }
    T& operator[](size_t index) { return m_ptr[index]; }
    int use_count() const { return *m_count; }  // 获取引用计数

private:
    T* m_ptr;              // 原始指针,指向共享的资源
    atomic<int>* m_count;  // 引用计数,记录有多少个 shared_ptr 实例共享同一个资源
    // 为了能够在这里也使用上模板参数 D,可以定义在类模板中
    // 或者直接使用 std::function<void(T*)> 来保存删除器,这样就不需要模板参数 D 了
    // 默认删除器,使用 lambda 表达式来定义一个默认的删除器,调用 delete 来释放资源
    function<void(T*)> m_del = [](T* ptr) { delete ptr; };
};

1.5 认识 weak_ptr

weak_ptr不拥有目标资源的所有权 ,也不直接支持访问资源(没有operator*和operator->),所以可以发现weak_ptr构造时不支持直接绑定到原始资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数。weak_ptr符合RAII设计,其构造和析构会正确管理控制块中的弱引用计数(weak_count)

weak_ptr也没有重载operator*和operator->等,因为它不参与资源管理,那么如果它绑定的 shared_ptr 已经释放了资源,那么它去访问资源就是很危险的。

它只有以下接口:

weak_ptr 支持expired 检查指向的资源是否过期use_count 也可获取shared_ptr的引用计数,weak_ptr 想访问资源时,可以调用 lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

示例代码:

cpp 复制代码
std::shared_ptr<string> sp1(new string("string"));
std::shared_ptr<string> sp2(sp1);    // sp1 和 sp2 共享 string 对象的所有权
std::weak_ptr<string> wp = sp1;      // wp 是 sp1 的弱引用,不增加 string 对象的引用计数

cout << wp.use_count() << endl;  // 输出 2,wp 不增加 string 对象的引用计数
cout << wp.expired() << endl;    // 输出 0,wp 没有过期,仍然可以访问 string 对象

// sp1 释放对原来 string 对象的所有权,增加对新 string 对象的所有权,sp2 仍然拥有原来 string 对象的所有权
sp1 = make_shared<string>("new string");
cout << wp.use_count() << endl;  // 输出 1,wp 不增加 string 对象的引用计数
cout << wp.expired() << endl;    // 输出 0,wp 没有过期,仍然可以访问原来 string 对象

// sp2 释放对原来 string 对象的所有权,增加对新 string 对象的所有权
sp2 = make_shared<string>("another string");
cout << wp.use_count() << endl;  // 输出 0,wp 不增加 string 对象的引用计数
cout << wp.expired() << endl;    // 输出 1,wp 已经过期,无法访问原来 string 对象

示例结果

weak_ptr 要访问资源,一定是 lock 出一个新的 shared_ptr 对象。

模拟实现 weak_ptr:

cpp 复制代码
template <class T>
class weak_ptr
{
public:
    weak_ptr() : m_ptr(nullptr) {}
    weak_ptr(const shared_ptr<T>& sp) : m_ptr(sp.m_ptr) {}
    weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        m_ptr = sp.m_ptr;
        return *this;
    }

private:
    T* m_ptr;        // 原始指针,指向共享的资源
};

1.6 总结

什么是RAII?

RAII是Resource Acquisition Is Initialization(资源获取即初始化)的缩写,是一种管理资源的类设计思想。

核心机制 :利用对象的生命周期来管理获取到的动态资源。资源在对象构造时获取并委托给对象,在对象析构时自动释放。

解决的问题

  • 避免资源泄漏(内存、文件句柄、网络连接、锁等)
  • 异常安全问题:异常发生时栈展开,局部对象析构,资源自动释放

智能指针是RAII的典型应用:将原始指针封装为对象,析构时自动delete,无需手动释放。


auto_ptr,unique_ptr,shared_ptr 各自的特点。

特性 auto_ptr (C++98) unique_ptr (C++11) shared_ptr (C++11)
所有权语义 独占,但支持拷贝 独占 共享
拷贝语义 拷贝时转移所有权,原对象置null 禁止拷贝 ,只允许移动 拷贝时引用计数+1,共享所有权
核心问题 转移所有权后原对象悬空,误访问导致UB 安全,明确表达独占语义 循环引用问题
使用建议 已废弃,禁止使用 不需要共享时使用 需要共享所有权时使用

unique_ptr vs shared_ptr关键区别

  • unique_ptr:零开销,只有一个指针开销,明确资源归属
  • shared_ptr:有引用计数开销(控制块),支持共享但需注意循环引用

shared_ptr 常见的问题以及怎么解决的。

引用计数设计

为什么不能是static成员**?所有对象共享同一个计数,无法区分不同资源** 。正确设计:堆上动态分配的控制块

循环引用问题

场景:双向链表、树结构中的父子互相引用。

死锁逻辑:n1释放需要n2的_prev析构 → n2释放需要n1的_next析构 → 互相等待,无法释放。

解决原理:weak_ptr打破循环。

核心机制 :weak_ptr只观察,不拥有 ,绑定到shared_ptr时不增加引用计数


2. 内存泄漏

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:普通程序运行一会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。最危险的一般是慢泄漏

如何避免内存泄漏?内存泄漏非常常键,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具

内存泄漏检测的原理:定义一个容器,申请资源时,将指针存在容器中;释放资源时,在容器中查找对应的指针,并删除。程序结束时,记录资源的各种属性,什么时候申请的等等,哪些资源还没有释放等等。


相关推荐
hhb_6182 小时前
Python 工程化开发与性能优化实践
开发语言·python·性能优化
前端摸鱼匠2 小时前
【AI大模型春招面试题23】大模型的参数量、计算量如何计算?FLOPs与FLOPS的区别?
开发语言·人工智能·面试·求职招聘·batch
董董灿是个攻城狮2 小时前
马斯克在用炸火箭的方式训练 AGI。。。
算法
江-月*夜2 小时前
vue3 wordcloud2.js词云使用
开发语言·javascript·vue.js
NiKick2 小时前
Python 爬虫实战案例 - 获取社交平台事件热度并进行影响分析
开发语言·爬虫·python
Pentane.2 小时前
【力扣hot100】【Leetcode 54】螺旋矩阵|边界控制 算法笔记及打卡(19/100)
算法·leetcode·矩阵
黎阳之光2 小时前
黎阳之光:港口智能体集群,重塑智慧港口新范式
大数据·人工智能·算法·安全·数字孪生
大写的z先生2 小时前
【深度学习 | 论文精读】
深度学习·算法·语言模型
大肥羊学校懒羊羊2 小时前
质因数个数问题:高效分解算法详解
开发语言·c++·算法