智能指针介绍
普通指针存在的问题
在C/C++开发时,指针导致的问题数不胜数。通常有如下几类问题:
- 内存泄漏:在程序运行期间,通过malloc系列函数或者C++的new申请了内存,但是忘记free或delete了。一旦这种情况反复出现,随着时间累积最终耗尽系统内存,引发系统异常。linux内核会监测到这种情况,主动杀掉内存占用异常的进程。
- 野指针:野指针是指未给指针正确初始化,所谓正确初始化就是要么是NULL/nullptr,要么是malloc/new出来的指针。比如全局指针变量,局部指针变量,未正确初始化就使用它,此时它指向的是一块谁也不知道在哪里的内存,一旦使用必然引发不可预知的后果。
- 悬空指针:悬空指针是指该指针指向已经释放的内存,此时对该指针指向的这块内存(也许它以属于别的进程)进行任何读写操作都会引发不可预知的问题,如果不小心再次调用free/delete释放这块内存会报double free错误。通常一个良好的习惯是释放完指针后立即把它置空,避免悬空指针引发的问题,因为对空指针进行释放操作是不会引起任何问题的,库中做好了判断,啥也处理。当然使用这个指针访问指向的内存也要做好判空处理。
以上这些陷阱尽管你小心翼翼的处理可以避免,但是一旦代码规模上去,复杂度上去,不可避免的还是会中招。好在C++引入了智能指针作为标准模版库(STL)的一部分,利用规则限定,避免踩入陷阱。在进一步了解智能指针时,需要了解另一个概念:RAII。
RAII概念
RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入;
是的,RAII非常巧妙里利用了局部变量(又称栈变量)在脱离作用域时,会自动销毁的原理(基本类型的局部变量就是通过弹栈,复合类型的会先调用析构函数,然后弹栈。这一切是由编译器在汇编过程自动实现的),来实现资源管理,比如此处将要讨论的通过malloc/new在堆上的内存。
智能指针
智能指针是一个指针的wrapper类,它像指针一样工作,但自动管理它所指向的内存。它确保在不再需要时正确释放内存,防止内存泄漏。它是
智能指针类型
C++ 库提供了以下类型的智能指针实现:
auto_ptrunique_ptrshared_ptrweak_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个条件:
-
- 删除器是invacable的
-
- 删除器接受一个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对象共享一个资源的示意图:
多个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_ptr的unique()实际就是判断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个共享指针:栈上的node1和node1管理的这块堆上Node类型内部的next,用变量表示就是node1->next。
其中黄色表示内存位于栈,绿色表示内存位于堆(下同)。
此时栈上的shared_ptr对象node1的ptr指向堆上分配的Node这块内存,node1的ref也就为1。而堆上的Node内部的shared_ptr对象next还是默认构造的状态,也就是ref=0,ptr=nullptr;
第二句:std::shared_ptr node2 = std::make_shared();
同node1一样,node2的内存分布,控制块的值如下:
第三句:node1->next = node2;
共享指针赋值操作会执行:
-
step1:
node1->next引用计数-1;由于
node1->next开始引用计数就是0,ptr也是nullptr,所以表示没管理任何资源,不做任何操作。 -
step2:
node1->next的控制块改为node2的;这一步时,相当于让
node1->next和node2共用一个控制块;此时node1->next的ptr指向原来node2指向的那块堆内存Node2,相应的node1->next的ref也就为1了。 -
step3:新的
node1->next引用计数+1;
node1->next的ref自增1,由原来的1变成了2。
以上过程由模板实现保证了是原子操作。最终样子如下:红色表示变化的地方。
第四句:node2->next = node1;
同样的步骤,执行赋值操作后,效果如下,其中红色表示变化部分。
(目前还有误,示意图需要修改)

浙公网安备 33010602011771号