智能指针介绍

普通指针存在的问题

  在C/C++开发时,指针导致的问题数不胜数。通常有如下几类问题:

  1. 内存泄漏:在程序运行期间,通过malloc系列函数或者C++的new申请了内存,但是忘记free或delete了。一旦这种情况反复出现,随着时间累积最终耗尽系统内存,引发系统异常。linux内核会监测到这种情况,主动杀掉内存占用异常的进程。
  2. 野指针:野指针是指未给指针正确初始化,所谓正确初始化就是要么是NULL/nullptr,要么是malloc/new出来的指针。比如全局指针变量,局部指针变量,未正确初始化就使用它,此时它指向的是一块谁也不知道在哪里的内存,一旦使用必然引发不可预知的后果。
  3. 悬空指针:悬空指针是指该指针指向已经释放的内存,此时对该指针指向的这块内存(也许它以属于别的进程)进行任何读写操作都会引发不可预知的问题,如果不小心再次调用free/delete释放这块内存会报double free错误。通常一个良好的习惯是释放完指针后立即把它置空,避免悬空指针引发的问题,因为对空指针进行释放操作是不会引起任何问题的,库中做好了判断,啥也处理。当然使用这个指针访问指向的内存也要做好判空处理。

以上这些陷阱尽管你小心翼翼的处理可以避免,但是一旦代码规模上去,复杂度上去,不可避免的还是会中招。好在C++引入了智能指针作为标准模版库(STL)的一部分,利用规则限定,避免踩入陷阱。在进一步了解智能指针时,需要了解另一个概念:RAII

RAII概念

  RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入;

是的,RAII非常巧妙里利用了局部变量(又称栈变量)在脱离作用域时,会自动销毁的原理(基本类型的局部变量就是通过弹栈,复合类型的会先调用析构函数,然后弹栈。这一切是由编译器在汇编过程自动实现的),来实现资源管理,比如此处将要讨论的通过malloc/new在堆上的内存。

智能指针

  智能指针是一个指针的wrapper类,它像指针一样工作,但自动管理它所指向的内存。它确保在不再需要时正确释放内存,防止内存泄漏。它是 头文件的一部分。智能指针是作为模板类实现的,因此我们可以创建指向任何类型内存的智能指针。

智能指针类型

C++ 库提供了以下类型的智能指针实现:

  1. auto_ptr
  2. unique_ptr
  3. shared_ptr
  4. weak_ptr

其中auto_ptr已在C++11中提示废弃,在C++17中被移除,所以不讨论它了。

unique_ptr

  unique表示唯一的,仅有的,这里一般称为独占指针,它表示独占所有权,同一时间只能有一个unique_ptr指向某个对象,不可复制,只能移动。它是通过删除拷贝构造函数和拷贝赋值运算符,同时提供移动构造函数和移动赋值运算符实现的。unique_ptr内部有一个指针,指向它要管理对象的内存。

智能指针unique_ptr是构造一个类型为T的指针wrapper模版类,该类模版大致样子为:

template <typename T>
class unique_ptr
{
public:
    unique_ptr(T *ptr)
        : ptr(ptr)
    {
    }

private:
    T* ptr;
};

这里省略了很多细节,主要是为了说明我之前的一个理解误区:以为智能指针模版类指定的这个类型必须是个指针。实际上是你指定一个类型T,它在内部定义一个T类型的指针T* ptr,然后通过unique_ptr构造函数初始化成员ptr

使用示例:

#include <iostream>
#include <memory>

class Data
{
public:
    Data()
    {
        std::cout << "Data::Data()" << std::endl;
    }

    ~Data()
    {
        std::cout << "Data::~Data()" << std::endl;
    }
};

int main(int argc, char *argv[])
{
    std::unique_ptr<Data> p1 = std::make_unique<Data>();

    /*std::unique_ptr<Data> p2(p1); //调用拷贝构造函数,提示已删除*/

    /*std::unique_ptr<Data> p3;
    p3 = p1; // 调用赋值操作符重载函数,提示已删除*/

    /*std::unique_ptr<Data> p4(std::move(p1)); //允许移动语义*/
    return 0;
}

借助RAII机制,局部变量p1在离开作用域时会自动调用其析构函数,而在模版类std::unique_ptr的析构函数中又会显式调用类型Data的析构函数,触发连锁析构,到达利用局部变量生命周期管理堆变量生命周期的效果。

再看看这种设计,解决了指针的哪些问题?

  • 利用RAII机制:

    上面也说过,局部变量脱离作用域自动释放的特点,加上模版类里面的连锁析构,从而杜绝了只new,不free的情况发生,内存泄漏被堵住了。

  • 删除了拷贝和赋值:避免多个指针指向这块内存,从而杜绝悬空指针的可能。悬空指针的产生有两种情况:

    • 单个指针指向这块内存,释放后,没置空,导致表面看上去这个指针依然可用。
    • 多个指针指向这块内存,A指针释放,并且也置空了,但是B没这么做。

对于第一种情况,由于智能指针定义的是一个类模版,在该类模版的析构函数里面,调用完被管理对象的析构函数后执行置空操作,大致的代码如下:

    delete ptr;     //调用管理对象的析构
    ptr = nullptr;  //置空

模版类这块是标准库里面的,只要你用它管理你的堆对象,那么在析构时必然不会出现未置空的问题。

对于第二种情况,正是因为删除了拷贝和赋值,这样就不存在多个unique_ptr的成员ptr指向同一块内存的现象,这种情况也不会发生了。

  • 类模版包裹堆指针:

    在该类模版的构造里面会初始化好这个被包裹的指针,保证它正确初始化,野指针也堵住了。

自此裸指针的三个陷阱被智能指针完美解决。那为什么要实现移动语义呢?因为有如下应用场景:

  • 从函数返回unique_ptr
std::unique_ptr<Data> create_resource {
    return std::make_unique<Data>();// 隐式移动(编译器优化为 NRVO)
}

无需拷贝(也无法拷贝),直接通过移动转移所有权。

  • 存入容器
std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(100));  // 必须显式移动

容器存储时必须使用 std::move(拷贝被禁用)。

  • 多态对象管理
std::unique_ptr<Base> obj = std::make_unique<Derived>();

移动语义支持基类指针安全接管派生类对象。

以上本来是要调用拷贝构造函数或者赋值操作符的,但是现在被删了,提供的是移动构造,所以调用的都是移动构造函数了。

make_unique是用来创建智能指针对象专门的方法,实际上:

std::unique_ptr<Data> p(new Data);

也是可以的,但是这种写法有一些缺陷:

  • 存在裸指针误用的情况:

    {
        Data *raw_p = new Data();
        std::unique_ptr<Data> p(raw_p);
        delete raw_p; //第一次释放
    }//脱离作用域,触发智能指针的第二次释放,double free错误!!
    
  • 异常导致的内存泄漏:

    void foo(std::unique_ptr<Data> p)
    {
    }
    
    int main(int argc, char *argv[])
    {
        foo(std::unique_ptr<Data>(new Data()));
        
        return 0;
    }
    

    当执行 std::unique_ptr p(new Data); 时:

    sequenceDiagram participant Program participant CPU participant Heap Program->>CPU: 执行 'new Data' CPU->>Heap: 1. 分配内存 (malloc) Heap-->>CPU: 返回裸指针 ptr* CPU->>+Data: 2. 构造对象 (Data的构造函数) Data-->>-CPU: 对象构造完成 CPU->>unique_ptr: 3. 调用 unique_ptr 构造函数 unique_ptr->>unique_ptr: 内部指针成员 = ptr* unique_ptr-->>Program: 构造完成

    在第 1~2步之间,裸指针 ptr* 已经存在但 未被任何机制管理:

  • new Data 已分配内存并返回指针。

  • Data 的构造函数 可能抛出异常(例如构造函数内有逻辑错误)。

  • 构造函数抛出异常 → unique_ptr 构造函数尚未执行。

此时:内存已分配,但所有权未转移给 unique_ptr → 内存泄漏!

unique_ptr接口介绍

假设使用下面这个类作为unique_ptr的类型:

class Data
{
public:
    Data()
        : data(0)
    {
        std::cout << "constructing Data" << std::endl;
    }

    explicit Data(int d)
        : data(d)
    {
        std::cout << "constructing Data" << std::endl;
    }

    ~Data()
    {
        std::cout << "deconstructing Data" << std::endl;
    }

private:
    int data;
};

基础构造

// 无参数
std::unique_ptr<Data> d1 = std::make_unique<Data>();
std::unique_ptr<Data> d3(new Data);

// 有参数
std::unique_ptr<Data> d2 = std::make_unique<Data>(4);
std::unique_ptr<Data> d4(new Data(7));

从上面例子看,说明make_unique这个模版函数是支持变参的。

获取裸指针

std::unique_ptr<Data> d = std::make_unique<Data>(4);

// 获取裸指针,但是d仍然有Data对象所有权
std::cout << d.get() << std::endl;

// 使用裸指针访问Data成员
Data *ptr = d.get();
ptr->showData();
//也可以写成下面样子,"->"表示指针成员访问符,但是IDE会提示“Redundant get() call on smart pointer(智能指针上的冗余 get() 调用)”,直接使用它的操作符重载函数即可。
d.get()->showData();

// 一步到位,注意这里的“->”是个操作符重载函数
d->showData();

通过get()方法获取unique_ptr内部管理的Data *类型指针ptr;获取原始指针后,并未发生所有权转移,智能指针对象d依然管理着ptr指向的那边内存的生命周期。获取裸指针通常是想访问T的成员。unique_ptr也实现了对“->”的重载,所以原本的d.get()->showData();直接可写为d->showData()

所有权转移

std::unique_ptr<Data> d1 = std::make_unique<Data>(4);
std::cout << "所有权转移前的指针:" << d1.get() << std::endl;
std::unique_ptr<Data> d2 = std::move(d1);
std::cout << "所有权转移后的指针:" << d1.get() << std::endl;
std::cout << "所有权转移后获得所有权的指针:" << d2.get() << std::endl;

运行结果:

constructing Data
所有权转移前的指针:0x5fea1d5f4eb0
所有权转移后的指针:0
所有权转移后获得所有权的指针:0x5fea1d5f4eb0
deconstructing Data

std::unique_ptr<Data> d2 = std::move(d1);这句是通过智能指针的移动构造函数实现对d2的构造的。

释放所有权

std::unique_ptr<Data> d1 = std::make_unique<Data>(1);

//提前获取内部指针
Data *ptr = d1.get();
//释放所有权
d1.release();
//由于释放了所有权,智能指针d不再管理内部指针指向的内存,所以需要手动释放
delete ptr;

std::unique_ptr<Data> d2 = std::make_unique<Data>(5);
//实际release带有返回值,就是内部指针,可以直接释放
delete d2.release();

所谓所有权,就是负责管理指针指向内存生命周期的权限。一旦释放所有权,那它就不再在自己析构时也析构它管理的对象了,所以需要自己显式释放。release实现大致为(这里主要讲它会做什么,实际的代码考虑的比这复杂得多,难以阅读):

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

重置内部指针

std::unique_ptr<Data> d1 = std::make_unique<Data>(1);
// 用新的指针替换掉老的内部指针,替换前会先释放老指针指向的内存
d1.reset(new Data(2));

std::unique_ptr<Data> d2 = std::make_unique<Data>(1);
// 也可以不指定新指针,实际就是nullptr。这样相当于提前结束了被管理对象的生命周期。
d2.reset();

reset用于替换内部指针,如果未指定新指针,则默认替换为nullptr,并且在替换前会先释放原来的内存。其内部实现大致为(这里主要讲它会做什么,实际的代码考虑的比这复杂得多,难以阅读):

void reset(T *p = nullptr)
{
    delete ptr;
    ptr = p; 
}

交换指针

std::unique_ptr<Data> d1 = std::make_unique<Data>(1);
std::unique_ptr<Data> d2 = std::make_unique<Data>(2);
// 交换d1和d2的指针
d1.swap(d2);

自定义删除器

如果使用自定义删除器,那就不能使用make_unqiue了,只能传递指针进去。

void FunctionDeleter(Data *d)
{
    std::cout << "call Function" << std::endl;
    delete d;
}

class Functor
{
public:
    void operator()(const Data *d) const
    {
        std::cout << "call Functor" << std::endl;
        delete d;
    }
};


// 使用仿函数作为删除器
std::unique_ptr<Data, Functor> d1(new Data(1), Functor());

//使用普通函数作为删除器
std::unique_ptr<Data, decltype(&FunctionDeleter)> d2(new Data(1), FunctionDeleter);

//使用lambda作为删除器
auto deleter = [](Data *p) {
    std::cout << "call Lambda" << std::endl;
    delete p;
};
std::unique_ptr<Data, decltype(deleter)> d3(new Data(1), deleter);

// 类静态成员函数,实际和普通函数一回事
std::unique_ptr<Data, decltype(&Data::staticMemberFunction)> d4(new Data(1), &Data::staticMemberFunction);

所谓删除器,就是unique_ptr自己析构时,去析构内部指针对象时用的方法,默认情况下就是内部的默认方法,查看源码,它实际是一个模版结构体,里面重载了()操作符,就是一个仿函数。

/// Primary template of default_delete, used by unique_ptr
template<typename _Tp>
struct default_delete
{
    /// Default constructor
    constexpr default_delete() noexcept = default;

    /** @brief Converting constructor.
     *
     * Allows conversion from a deleter for arrays of another type, @p _Up,
     * only if @p _Up* is convertible to @p _Tp*.
     */
    template<typename _Up, typename = typename
        enable_if<is_convertible<_Up*, _Tp*>::value>::type>
    default_delete(const default_delete<_Up>&) noexcept { }

    /// Calls @c delete @p __ptr
    void
    operator()(_Tp* __ptr) const
    {
        static_assert(!is_void<_Tp>::value,
                    "can't delete pointer to incomplete type");
        static_assert(sizeof(_Tp)>0,
                    "can't delete pointer to incomplete type");
        delete __ptr;
    }
};

如果定义unique_ptr时,也指定了删除器类型,那就用你指定的。作为删除器需要满足2个条件:

    1. 删除器是invacable的
    1. 删除器接受一个T*类型的参数

至于为啥,可以看看~unique_ptr的实现:

/// Destructor, invokes the deleter if the stored pointer is not null.
~unique_ptr() noexcept
{
    //断言中首先要求是:invocable的,然后要还要支持一个pointer参数,pointer实际就是using pointer = T*。
    static_assert(__is_invocable<deleter_type&, pointer>::value,"unique_ptr's deleter must be invocable with a pointer");
    auto& __ptr = _M_t._M_ptr();
    if (__ptr != nullptr)
    {
        //获取删除器,然后把内部指针__ptr以移动语义作为删除器的参数
        get_deleter()(std::move(__ptr));
    }
    //置空
    __ptr = pointer();
}

例子中,当删除非仿函数时,巧妙的利用decltype获取到这些对象的类型。实际上自己写完整的类型也行:

    /*使用普通函数作为删除器*/
    // 利用decltype自动推导
    std::unique_ptr<Data, decltype(&FunctionDeleter)> d21(new Data(1), FunctionDeleter);
    // 手动推断
    std::unique_ptr<Data, void(*)(Data *p)> d22(new Data(1), FunctionDeleter);

数组特化

unique_ptr也支持类型为数组

    std::unique_ptr<Data[]> data = std::make_unique<Data[]>(2);
    data[0] = Data(1);
    data[1] = Data(2);

    //越界也可以,实际要使用者自己规避这种情况
    data[6] = Data(3);

shared_ptr

  共享指针,和独占指针unique_ptr不同,它允许多个对象管理同一个指针。它内部除了被管理的指针对象,还有一个引用计数,表示有多少个shared_ptr对象同时管理着一个共同的指针,当这个引用计数为0时,就开始析构。

  共享指针内部有两个指针,一个指向管理资源的指针(ptr),还有一个控制块指针(ctrl_block)。

                        内存布局
┌─────────────┐     ┌──────────────┐
│ shared_ptr  │     │ control_block│
├─────────────┤     ├──────────────┤
│ ptr      ───┼─────►  object_ptr  │
│ ctrl_block ─┼─────►  ref_count   │
└─────────────┘     │  weak_count  │
                    │  deleter     │
                    └──────────────┘

控制块指针指向的这片内存中有一个共享指针实现的关键字段:ref_count,引用计数。多个shared_ptr对象共享一个资源的示意图:

flowchart LR subgraph shared_ptrs sp1[shared_ptr instance 1] sp2[shared_ptr instance 2] sp3[shared_ptr instance 3] end subgraph control_block[Control Block] direction TB shared_count[ref_count = 3] end resource[Managed Resource] sp1 --> |ptr| resource sp1 --> |control_block| control_block sp2 --> |ptr| resource sp2 --> |control_block| control_block sp3 --> |ptr| resource sp3 --> |control_block| control_block

多个shared_ptr对象会指向同一个被管理资源,也会共用同一个控制块。

shared_ptr接口介绍

有如下类Data作为例子中被管理资源类型:

class Data
{
public:
    Data()
    {
        std::cout << __FUNCTION__ << std::endl;
    }

    ~Data()
    {
        std::cout << __FUNCTION__ << std::endl;
    }
};

基础构造

构造初始化如果没指定关联资源,也就是内部指针为nullptr;

std::shared_ptr<Data> sptr;
std::cout << "sptr 引用计数=" << sptr.use_count() << " 被管理资源的指针=" << sptr.get() << std::endl;

shared_ptr使用use_count获得当前引用计数,同unique_ptr一样,使用get获得被管理资源的指针。运行输出:

sptr 引用计数=0 被管理资源的指针=0

使用make_shared来进行构造:

std::shared_ptr<Data> sptr = std::make_shared<Data>();
std::cout << "sptr 引用计数=" << sptr.use_count() << " 被管理资源的指针=" << sptr.get() << std::endl;

由于指定了被管理资源,因此引用计数自增。

Data
sptr 引用计数=1 被管理资源的指针=0x64a58d0232c0
~Data

也可以使用裸指针构造,不过和unique_ptr介绍的一样,除非特殊情况下,一般不用这种方法,因为它有缺陷。

std::shared_ptr<Data> sptr(new Data());

拷贝构造

std::shared_ptr<Data> sptr = std::make_shared<Data>();
std::cout << "sptr 引用计数=" << sptr.use_count() << " 被管理资源的指针=" << sptr.get() << std::endl;

//拷贝构造,引用计数自增
std::shared_ptr<Data> sptr2(sptr);
std::cout << "sptr 引用计数=" << sptr.use_count() << " 被管理资源的指针=" << sptr.get() << std::endl;
std::cout << "sptr2 引用计数=" << sptr2.use_count() << " 被管理资源的指针=" << sptr2.get() << std::endl;

运行输出:

Data
sptr 引用计数=1 被管理资源的指针=0x5e5d9c1992c0
sptr 引用计数=2 被管理资源的指针=0x5e5d9c1992c0
sptr2 引用计数=2 被管理资源的指针=0x5e5d9c1992c0
~Data

赋值操作

std::shared_ptr<Data> sptr = std::make_shared<Data>();
std::cout << "sptr 引用计数=" << sptr.use_count() << " 被管理资源的指针=" << sptr.get() << std::endl;

std::shared_ptr<Data> sptr2;
std::cout << "sptr 引用计数=" << sptr.use_count() << " 被管理资源的指针=" << sptr.get() << std::endl;
std::cout << "sptr2 引用计数=" << sptr2.use_count() << " 被管理资源的指针=" << sptr2.get() << std::endl;
sptr2 = sptr;
std::cout << "sptr 引用计数=" << sptr.use_count() << " 被管理资源的指针=" << sptr.get() << std::endl;
std::cout << "sptr2 引用计数=" << sptr2.use_count() << " 被管理资源的指针=" << sptr2.get() << std::endl;

运行输出:

Data
sptr 引用计数=1 被管理资源的指针=0x5e9ef8f212c0
sptr 引用计数=1 被管理资源的指针=0x5e9ef8f212c0
sptr2 引用计数=0 被管理资源的指针=0
sptr 引用计数=2 被管理资源的指针=0x5e9ef8f212c0
sptr2 引用计数=2 被管理资源的指针=0x5e9ef8f212c0
~Data

获取裸指针

std::shared_ptr<Data> sptr = std::make_shared<Data>();
std::cout << sptr.get() << std::endl;

获取引用计数

std::shared_ptr<Data> sptr = std::make_shared<Data>();
std::cout << sptr.use_count() << std::endl;

判断是否为唯一引用

std::shared_ptr<Data> sptr = std::make_shared<Data>();
if (sptr.unique())
{
    std::cout << "current is unique reference, use_count:" << sptr.use_count() << std::endl;
}

shared_ptrunique()实际就是判断use_count() == 1

判断内部指针是否为空

std::shared_ptr<Data> sptr;
if (sptr)
{
    std::cout << "ptr is not nullptr";
}
else
{
    std::cout << "sptr is nullptr";
}

粗看这段代码莫名其妙,sptr是一个局部变量,也并非一个指针,也不是一个内置类型。按道理作为if判断的条件是会编译报错的,但是实际上不会,这是因为shared_ptr继承了__shared_ptr,而__shared_ptr实现了下面的方法:

/// Return true if the stored pointer is not null.
explicit operator bool() const noexcept
{ return _M_ptr != nullptr; }

操作符重载,重载了bool()操作符,它表示类型转换操作符。即这里重载了bool类型转换操作符。而if判断需要的类型就是一个bool类型,因此代码中if(sptr)实际触发了bool类型隐式转换即为if(bool(sptr)),因此会调用对应的重载函数,重载函数里面是判断内部指针是否为空,为空返回false,不为空返回true。实际上,智能指针,不管是unique_ptr还是shared_ptr都会通过操作符重载,让智能指针类型(注意它本质不是指针,而是自定义类型)尽可能的用起来像普通指针。除了这里的bool类型转换操作符重载,还有->操作符重载,*操作符重载:

element_type&
operator*() const noexcept
{
    __glibcxx_assert(_M_get() != nullptr);
    return *_M_get();
}

element_type*
operator->() const noexcept
{
    _GLIBCXX_DEBUG_PEDASSERT(_M_get() != nullptr);
    return _M_get();
}

这样使得对普通指针常用的操作,在智能指针上同样适用。

std::shared_ptr<Data> data = std::make_shared<Data>();
//隐式类型装换'bool()'
if (data)
{
    std::cout << "内部指针不为空" << std::endl;
    //结构体指针访问成员运算符
    data->foo();
    //指针解引用操作符
    (*data).foo();
}

不过智能指针上这些操作符重载,最终作用于内部指针上。其目的就是:
智能指针类型(注意它本质不是指针,而是自定义类型)尽可能的用起来像普通指针。

比较操作符重载

比较操作符的==><!=<=>=都被重载了。以==为例:

std::shared_ptr<Data> sptr1 = std::make_shared<Data>();
std::shared_ptr<Data> sptr2 = std::make_shared<Data>();
if (sptr1 == sptr2)
{
    std::cout << "sptr1 == sptr2" << std::endl;
}

if (sptr1 == nullptr)
{
    std::cout << "sptr1 == nullptr" << std::endl;
}

if (nullptr == sptr1)
{
    std::cout << "nullptr == sptr1" << std::endl;
}

三种写法,对应三个操作符重载函数模板,它们三个之间又彼此构成函数重载。其他5个操作符重载也是类似的,都有3个函数重载,都是针对内部指针操作。

重置内部指针

std::shared_ptr<Data> sptr = std::make_shared<Data>();
std::cout << "sptr 引用计数=" << sptr.use_count() << " 被管理资源的指针=" << sptr.get() << std::endl;
sptr.reset(new Data);
std::cout << "sptr 引用计数=" << sptr.use_count() << " 被管理资源的指针=" << sptr.get() << std::endl;
sptr.reset();
std::cout << "sptr 引用计数=" << sptr.use_count() << " 被管理资源的指针=" << sptr.get() << std::endl;

执行输出:

Data
sptr 引用计数=1 被管理资源的指针=0x5696396b02c0
Data
~Data
sptr 引用计数=1 被管理资源的指针=0x5696396b06e0
~Data
sptr 引用计数=0 被管理资源的指针=0

使用reset重置内部指针,如果带了新的资源指针,那么将先自减原来控制块的引用计数,如果此时引用计数为0,则析构资源。并分配新控制块,引用计数自增,内部指针指向新资源。如果reset参数为空,那么仅自减引用计数。

shared_ptr无release操作,因为多个共享指针管理一个资源,任意一个释放都不能保证其对象是否还管理着这块资源。

循环引用

共享指针存在一个问题,看下面的例子:

#include <iostream>
#include <memory>

class Node
{
public:
    std::shared_ptr<Node> next;

    explicit Node()
    {
        std::cout << "Node created" << std::endl;
    }

    ~Node()
    {
        std::cout << "Node destroyed" << std::endl;
    }
};

int main()
{
    std::shared_ptr<Node> node1 = std::make_shared<Node>();
    std::shared_ptr<Node> node2 = std::make_shared<Node>();

    // 形成循环引用
    node1->next = node2;
    node2->next = node1;

    std::cout << "node1 use_count: " << node1.use_count() << std::endl;
    std::cout << "node2 use_count: " << node2.use_count() << std::endl;

    return 0;
}

这是一个经典的例子,单向指针,彼此指向,形成循环引用。运行后会发现,两次std::make_shared<Node>()分配的内存都没有被释放。要理解为什么回这样,要分析一下main函数执行过程中这里面的共享指针内部是如何变化的。

第一句:std::shared_ptr node1 = std::make_shared();

栈上分配了一个共享指针node1,它管理堆上分配的一块Node类型内存,此时出现了2个共享指针:栈上的node1node1管理的这块堆上Node类型内部的next,用变量表示就是node1->next

graph LR n1ptr-->N1; subgraph n1["node1[std::shared_ptr]"] direction LR; n1ref[ref==1]; n1ptr[ptr]; end subgraph N1[Node] direction LR; subgraph N1next["next[std::shared_ptr]"] N1ref[ref==0]; N1ptr[ptr==nullptr]; end end style N1 fill:#00FF00 style N1next fill:#00FF00 style n1 fill:#FFFF00

其中黄色表示内存位于栈,绿色表示内存位于堆(下同)。

此时栈上的shared_ptr对象node1ptr指向堆上分配的Node这块内存,node1ref也就为1。而堆上的Node内部的shared_ptr对象next还是默认构造的状态,也就是ref=0,ptr=nullptr;

第二句:std::shared_ptr node2 = std::make_shared();

node1一样,node2的内存分布,控制块的值如下:

graph LR n1ptr-->N1; n2ptr-->N2; subgraph n1["node1[std::shared_ptr]"] direction LR; n1ref[ref==1]; n1ptr[ptr]; end subgraph N1[Node] direction LR; subgraph N1next["next[std::shared_ptr]"] N1ref[ref==0]; N1ptr[ptr==nullptr]; end end subgraph n2["node2[std::shared_ptr]"] direction LR; n2ref[ref==1]; n2ptr[ptr]; end subgraph N2[Node] direction LR; subgraph N2next["next[std::shared_ptr]"] N2ref[ref==0]; N2ptr[ptr==nullptr]; end end style N1 fill:#00FF00 style N1next fill:#00FF00 style n1 fill:#FFFF00 style N2 fill:#00FF00 style N2next fill:#00FF00 style n2 fill:#FFFF00
第三句:node1->next = node2;

共享指针赋值操作会执行:

  • step1:node1->next引用计数-1;

    由于node1->next开始引用计数就是0,ptr也是nullptr,所以表示没管理任何资源,不做任何操作。

  • step2:node1->next的控制块改为node2的;

    这一步时,相当于让node1->nextnode2共用一个控制块;此时node1->next的ptr指向原来node2指向的那块堆内存Node2,相应的node1->next的ref也就为1了。

  • step3:新的node1->next引用计数+1;
    node1->next的ref自增1,由原来的1变成了2。

以上过程由模板实现保证了是原子操作。最终样子如下:红色表示变化的地方。

graph LR n1ptr-->N1; n2ptr-->N2; N1ptr-->N2; subgraph n1["node1[std::shared_ptr]"] direction LR; n1ref[ref==1]; n1ptr[ptr]; end subgraph N1[Node1] direction LR; subgraph N1next["next[std::shared_ptr]"] N1ref[ref==1]; N1ptr[ptr==&Node2]; end end subgraph n2["node2[std::shared_ptr]"] direction LR; n2ref[ref==1]; n2ptr[ptr]; end subgraph N2[Node2] direction LR; subgraph N2next["next[std::shared_ptr]"] N2ref[ref==2]; N2ptr[ptr==nullptr]; end end style N1 fill:#00FF00 style N1next fill:#00FF00 style n1 fill:#FFFF00 style N2 fill:#00FF00 style N2next fill:#00FF00 style n2 fill:#FFFF00 style N2ref fill:#FF0000 style N1ptr fill:#FF0000
第四句:node2->next = node1;

同样的步骤,执行赋值操作后,效果如下,其中红色表示变化部分。

graph LR n1ptr-->N1; n2ptr-->N2; N1ptr-->N2; N2ptr-->N1; subgraph n1["node1[std::shared_ptr]"] direction LR; n1ref[ref==1]; n1ptr[ptr]; end subgraph N1[Node1] direction LR; subgraph N1next["next[std::shared_ptr]"] N1ref[ref==2]; N1ptr[ptr==&Node2]; end end subgraph n2["node2[std::shared_ptr]"] direction LR; n2ref[ref==1]; n2ptr[ptr]; end subgraph N2[Node2] direction LR; subgraph N2next["next[std::shared_ptr]"] N2ref[ref==2]; N2ptr[ptr==&Node1]; end end style N1 fill:#00FF00 style N1next fill:#00FF00 style n1 fill:#FFFF00 style N2 fill:#00FF00 style N2next fill:#00FF00 style n2 fill:#FFFF00 style N1ref fill:#FF0000 style N2ptr fill:#FF0000

(目前还有误,示意图需要修改)

posted @ 2025-06-24 16:03  thammer  阅读(28)  评论(0)    收藏  举报