shared_ptr的概念
shared_ptr实现共享式拥有(shared ownership)概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用(reference)被销毁”时候释放。
基本原理
智能指针是(几乎总是)模板类,shared_ptr 同样是模板类,所以在创建 shared_ptr 时需要指定其指向的类型。shared_ptr 负责在不使用实例时释放由它管理的对象,同时它可以自由的共享它指向的对象。
shared_ptr 使用经典的 “引用计数
” 的方法来管理对象资源。
模拟实现理解原理
在系统提供的共享性智能指针中,存在一个观察器use_count
用来返回shared_ptr
所管理对象的引用计数,使用样例如下:
那么,其具体实现是怎样的呢?我们来模仿源码实现一下:
template<class _Ty>
class RefCnt
{
public:
_Ty* mptr;
//int ref;
std::atomic_int ref;
public:
RefCnt(_Ty* p = nullptr) :mptr(p), ref(mptr != nullptr) { //ref = mptr == nullptr?0:1;}
~RefCnt() {}
};
template<class _Ty, class _Dx = MyDeletor<_Ty> >
class my_shared_ptr // thread;
{
public:
my_shared_ptr(_Ty* p = nullptr) :ptr(nullptr)
{
if (p != nullptr)
{
ptr = new RefCnt(p);
}
}
my_shared_ptr(const my_shared_ptr& _Y) :ptr(_Y.ptr)
{
if (ptr != nullptr)
{
ptr->ref += 1;
}
}// my_shared_ptr<Object> op2(op1);
my_shared_ptr(my_shared_ptr&& _Y) :ptr(_Y.ptr)
{
_Y.ptr = nullptr;
}// my_shared_ptr<Object> op2(std::move(op1));
operator bool() const { return ptr != nullptr; }
my_shared_ptr& operator=(const my_shared_ptr& _Y) //
{
if (this == &_Y || this->ptr == _Y.ptr) return *this;
if (ptr != NULL && --ptr->ref == 0)
{
mDeletor(ptr);
}
ptr = _Y.ptr;
if (ptr != nullptr)
{
ptr->ref += 1;
}
return *this;
}
my_shared_ptr& operator=(my_shared_ptr&& _Y) // move operator =
{
if (this == &_Y) return *this;
if (this->ptr == _Y.ptr && this->ptr != nullptr && _Y.ptr != nullptr)
{
this->ptr->ref -= 1;
_Y.ptr = nullptr;
return * this;
}
if (this->ptr != nullptr && --ptr->ref == 0)
{
mDeletor(ptr);
}
ptr = _Y.ptr;
_Y.ptr = nullptr;
return *this;
}
void reset(_Ty* p = nullptr)
{
if (this->ptr != nullptr && --this->ptr->ref == 0)
{
mDeletor(ptr);
}
ptr = new RefCnt<_Ty>(p);
}
~my_shared_ptr()
{
if (this->ptr != nullptr && --this->ptr->ref == 0)
{
mDeletor(ptr->mptr);
delete ptr;
}
ptr = nullptr;
}
_Ty* get() const { return ptr->mptr; }
_Ty& operator*() const
{
return *get();
}
_Ty* operator->() const
{
return get();
}
size_t use_count() const
{
if (this->ptr == nullptr) return 0;
return this->ptr->ref;
}
void swap(my_shared_ptr& r)
{
std::swap(this->ptr, r.ptr);
}
private:
RefCnt<_Ty>* ptr;
_Dx mDeletor;
};
template<class _Ty, class _Dx >
class my_shared_ptr<_Ty[],_Dx>
{
public:
my_shared_ptr(_Ty* p = nullptr) :ptr(nullptr)
{
if (p != nullptr)
{
ptr = new RefCnt(p);
}
}
my_shared_ptr(const my_shared_ptr& _Y) :ptr(_Y.ptr)
{
if (ptr != nullptr)
{
ptr->ref += 1;
}
}// my_shared_ptr<Object> op2(op1);
my_shared_ptr(my_shared_ptr&& _Y) :ptr(_Y.ptr)
{
_Y.ptr = nullptr;
}// my_shared_ptr<Object> op2(std::move(op1));
operator bool() const { return ptr != nullptr; }
my_shared_ptr& operator=(const my_shared_ptr& _Y) //
{
if (this == &_Y || this->ptr == _Y.ptr) return *this;
if (ptr != NULL && --ptr->ref == 0)
{
mDeletor(ptr->mptr);
delete ptr;
}
ptr = _Y.ptr;
if (ptr != nullptr)
{
ptr->ref += 1;
}
return *this;
}
my_shared_ptr& operator=(my_shared_ptr&& _Y) // move operator =
{
if (this == &_Y) return *this;
if (this->ptr == _Y.ptr && this->ptr != nullptr && _Y.ptr != nullptr)
{
this->ptr->ref -= 1;
_Y.ptr = nullptr;
return * this;
}
if (this->ptr != nullptr && --ptr->ref == 0)
{
mDeletor(ptr->mptr);
delete ptr;
}
ptr = _Y.ptr;
_Y.ptr = nullptr;
return *this;
}
void reset(_Ty* p = nullptr)
{
if (this->ptr != nullptr && --this->ptr->ref == 0)
{
mDeletor(ptr->mptr);
delete ptr;
}
ptr = new RefCnt<_Ty>(p);
}
~my_shared_ptr()
{
if (this->ptr != nullptr && --this->ptr->ref == 0)
{
mDeletor(ptr->mptr);
delete ptr;
}
ptr = nullptr;
}
_Ty* get() const { return ptr->mptr; }
_Ty& operator*() const
{
return *get();
}
_Ty* operator->() const
{
return get();
}
size_t use_count() const
{
if (this->ptr == nullptr) return 0;
return this->ptr->ref;
}
void swap(my_shared_ptr& r)
{
std::swap(this->ptr, r.ptr);
}
_Ty& operator[](const int idx) const
{
return ptr->mptr[idx];
}
private:
RefCnt<_Ty>* ptr;
_Dx mDeletor;
};
根据代码:我们分析到对象的内存结构图如下:这有助于我们更好地理解:
相互引用问题
退出之前,它们的 use_count() 都为2,退出了 fun() 后,由于 c和 p 对象互相引用,它们的引用计数都是 1,不能自动释放(可以看到没有调用析构函数),并且此时这两个对象再无法访问到。这就引起了c++中那臭名昭著的“内存泄漏”。
我们解决这个问题就可以使用weak_ptr
:
使用weak_ptr 来打破循环引用,它与一个 shared_ptr 绑定,但却不参与引用计数的计算,不论是否有 weak_ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。weak_ptr 像是 shared_ptr 的一个助手。同时,在需要时,它还能摇身一变,生成一个与它绑定的 shared_ptr 共享引用计数的shared_ptr。
多线程下的shared_ptr存在的问题
因为在多线程下,多个线程很有可能对同一个shared_ptr
的引用计数进行操作,然而在我们设计的shared_ptr
中的引用计数为int类型,对于int的一个操作其实并不是原子操作,那么多线程环境下就不能保证安全的运行。
于是我们会对引用计数进行一个重新的设计:
引入#include<atomic>
,修改引用计数的类型如下:
从而解决这一问题。
总结
(1) 智能指针主要的用途就是方便资源的管理,自动释放没有指针引用的资源。
(2) 使用引用计数来标识是否有多余指针指向该资源。(注意,shart_ptr本身指针会占1个引用)
(3) 在赋值操作中, 原来资源的引用计数会减一,新指向的资源引用计数会加一。
(4) 引用计数加一/减一操作是原子操作,所以线程安全的。
(5) make_shared要优于使用new,make_shared可以一次将需要内存分配好。(第一张图片中有解释)(下文有拓展
)
(6) std::shared_ptr的大小是原始指针的两倍,因为它的内部有一个原始指针指向资源,同时有个指针指向引用计数。
(7) 引用计数是分配在动态分配的,std::shared_ptr支持拷贝,新的指针获可以获取前引用计数个数。
拓展:使用make_shared的三个理由
- 减少在堆区构建对象的次数;
- 为了提高命中率,使被管理对象和引用计数结构在同一个空间,访问速度加快(因为因为计数器内存和原生内存排排坐)
- 在一些调用次序不确定的情况下,使得被管理对象仍然处于可控阶段;
对于第三点的解释:
上面的代码在执行过程中,有三件事情需要完成,构建Object
,构建shared_ptr
以及调用couldThrowException
(),C++17引入了更加严格的函数参数的构建顺序,但在那之前,上面三件事的构建顺序如下:
- new Object;
- 调用couldThrowException
- 构建sahred_ptr并管理步骤1的内存
所以存在的问题就是:一旦步骤二抛出异常,步骤三就不会发生,智能指针很无辜,这不是它造成的,然而一旦使用std::make_shared
的方式就会使得步骤1和3紧挨在一起,也就不会发生刚才的事情。