智能指针(2)
- shared_ptr(共享型智能指针)
- 基础知识
- 特点
- 引用计数器
- 共享型智能指针结构理解
- shared_ptr仿写
- 删除器类
- 计数器类
- shared_ptr类
- 使用以及仿写代码的理解
- 循环引用
- _Weaks
- 初始化智能指针的方法
shared_ptr(共享型智能指针)
基础知识
在java中有一个垃圾回收器,可以运用到所有资源,heap内存和系统资源都可以使用的系统,而C++11中的shared_ptr就是为了达到这样的目的,其和唯一性智能指针不同,共享型智能指针没有一个特定的指针拥有资源对象,而是这些指针指向同一资源对象,相互协作来管理该对象,在不需要时进行析构。
特点
- 共享所有权模式
- 使用了引用技术控制管理资源对象的生命周期
- 提供拷贝构造函数和赋值重载函数;提供移动构造和移动赋值。
- 添加了删除器类型
- 在容器中保存shared_ptr对象是安全的
- 重载了operator->和operator*运算符,因此可以像普通指针一样使用
引用计数器
引用计数器的值为指向资源的智能指针数,当智能指针对象死亡时,判断引用计数器的值,如果不是1,智能指针本身析构,不释放资源,如果是1,析构智能指针,释放资源。
共享型智能指针结构理解
此处用自己设计的整型举例,不做编写
int main() {
std::shared_ptr<Int> pa(new Int(10));
cout << pa.use_count() << endl;
std::shared_ptr<Int>pb(pa);
cout << pa.use_count() << endl;
std::shared_ptr<Int> pc;
pc = pa;
cout << pa.use_count() << endl;
return 0;
}
其输出结果是这样的:
为什么会输出这样的结果呢?我们可以画图来理解智能指针的结构。
这里我只画了pc对象的结构,其结构都是一样的,创建pa对象的时候,其有三个成员对象,一个是删除器,另外两个是指针,指向堆区的空间,其中一个指针指向我们的Int对象,另一个指向我们在堆区创建的计数器对象,计数器中存在一个指针和两个计数器,其指针指向我们的Int对象,两个计数器分别是_Uses和_Weaks,这里个计数器分别是共享型智能指针和弱引用智能指针。这里暂时不给大家说说这两者的区别,后面会在循环引用中讲解,大家只需要知道当创建智能指针时,其资源数+1。所以呢我们输出结果才是123。
这里如果大家还不是很懂,我们再对其进行简单的仿写。
shared_ptr仿写
删除器类
删除器类型就是为了通过仿函数来析构对象,增强代码的可用性。
template<class _Ty>
struct my_deleter
{
void operator()(_Ty* ptr) const
{
delete ptr;
}
};
template<class _Ty>
struct my_deleter<_Ty[]>
{
void operator()(_Ty* ptr) const
{
delete[]ptr;
}
};
计数器类
class My_RefCount
{
public:
using element_type = _Ty;
using pointer = _Ty*;
private:
_Ty* _Ptr;
std::atomic<int> _Uses; // shared;
std::atomic<int> _Weaks; // weak_ptr;
public:
My_RefCount(_Ty* ptr = nullptr)
:_Ptr(ptr), _Uses(0), _Weaks(0)
{
if (_Ptr != nullptr)
{
_Uses = 1;
_Weaks = 1;
}
}
~My_RefCount() = default;
void Incref()
{
_Uses += 1;
}
void Incwref()
{
_Weaks += 1;
}
int Decref()
{
if (--_Uses == 0)
{
Decwref();
}
return _Uses;
}
int Decwref()
{
_Weaks -= 1;
return _Weaks;
}
int _use_count() const
{
return _Uses.load();
}
};
在计数器类型中成员方法就是将两个计数器进行+1,-1操作,返回当前指向资源的计数器。也没有太多要说的,主要是其结构,上面说了计数器类型,其有三个成员对象,分别是ptr指针,指向资源对象,而另外两个便是整型计数器,但是其写法是这样的 std::atomic<int> _Uses; // shared; std::atomic<int> _Weaks; // weak_ptr;
这样的写法是让其这两个计数器具有原子性。而原子性是什么呢?原子性:我们发现在多线程编程中,我们举一个例,有三个线程,和一个全局变量a=1,每个线程中对该变量进行++操作100次,我们可以发现结果有可能不是300,这是因为有几个线程同时对变量+1,导致两次+1操作本来+2,变成了+1一次,所以呢会小于300,有了原子性,便不会出现这样的操作,在++的时刻不会出现其他线程也+1操作。和锁的作用类似,但是大家对互斥锁的底层有所了解的话会知道其是从就绪态,运行态,阻塞态不停的切换,这样的话比该方法效率低得多。
shared_ptr类
template<class _Ty, class Deleter = my_deleter<_Ty> >
class my_shared_ptr
{
private:
_Ty* mPtr;
My_RefCount<_Ty>* mRep;
Deleter mDeleter;
public:
my_shared_ptr(_Ty* ptr = nullptr) :mPtr(ptr), mRep(nullptr)
{
if (mPtr != nullptr)
{
mRep = new My_RefCount(mPtr);
}
}
my_shared_ptr(const my_shared_ptr& src) :mPtr(src.mPtr), mRep(src.mRep)
{
if (mRep != nullptr)
{
mRep->Incref();
}
}
~my_shared_ptr()
{
if (mRep != nullptr && mRep->Decref() == 0)
{
mDeleter(mPtr);
delete mRep;
}
mPtr = nullptr;
mRep = nullptr;
}
my_shared_ptr(my_shared_ptr&& right) :
mPtr(right.mPtr), mRep(right.mRep) {
right.mPtr = nullptr;
right.mRep = nnullptr;
}
/*my_shared_ptr& operator=(const my_shared_ptr& src) {
if (this == &src) return*this;
if (mRep != nullptr && mRep->Decref() == 0) {
mDeleter(mPtr);
delete mRep;
}
mPtr = src.mPtr;
mRep = src.mRep;
if (mRep != nullptr) {
mRep->Incref();
}
return *this;
}*/
my_shared_ptr& operator=(const my_shared_ptr& src) {
my_shared_ptr(src).swap(*this);
}
/*my_shared_ptr& operator=(my_shared_ptr&& right) {
if (this == &right) return *this;
if (mRep != nullptr && mRep->Decref() == 0) {
mDeleter(mPtr);
delete mRep;
}
mPtr = right.mPtr;
mRep = right.mRep;
right.mPtr = nullptr;
right.mRep = nullptr;
}*/
my_shared_ptr& operator=(my_shared_ptr&& right) {
if (this == &right) return *this;
my_shared_ptr(std::move(right)).swap(*this);
return*this;
}
int use_count() const {
return mRep != nullptr ? mRep->_use_count() : 0;
}
_Ty* get()const {
return mPtr;
}
_Ty& operator*()const {
return *get();
}
_Ty* operator->()const {
return get();
}
operator bool()const {
return mPtr != nullptr;
}
void swap(my_shared_ptr& other) {
std::swap(this->mPtr, other.mPtr);
std::swap(this->mRep, other.mRep);
}
void reset() {
if (mRep->Decref() == 0) {
mDeleter(mPtr);
delete mRep;
}
mPtr = nullptr;
mRep = nullptr;
}
void reset(_Ty* p) {
if (nullptr == p) {
reset();
}
if (mRep->Decref() == 0) {
mDeleter(mPtr);
delete mRep;
}
mPtr = p;
mRep = new My_RefCount(ptr);
}
};
使用以及仿写代码的理解
构造函数:共享型指针的时候呢,使用指向资源的指针初始化对象中的指针,然后用该指针构建计数器对象。
拷贝构造:将原本智能指针类型的两个指针进行赋值,然后对计数器中的智能指针计数器+1操作。
析构函数:如果指向对象的指针不为nullptr并且其计数器自减之后不为0,将其两个指针置为nullptr,如果自减之后为0则进行释放资源对象和计数器对象。
移动构造:将其指针进行赋值然后将形参的两个指针置为nullptr
重载赋值运算符:这里呢有两种方法,第一种是判断原本对象自减之后是否为0,然后进行释放资源,然后将待赋值对象进行赋值,如果对象不为nullptr,则计数器+1,另一种用到了置换函数,也是共享型智能指针中源码的写法,很是奇妙。使用形参创建一个临时对象,然后将其和this指向的对象进行资源置换。创建的临时对象,现在存放的是 this指向原本的资源,然后因为是临时对象,所以最后会析构一次,调用析构函数,判断其计数器是否为0来决定其是否释放对象,而创建src对象的时候便对其计数器进行了+1操作,然后资源置换给了this指针。
移动语义的赋值运算符重载:这里和重载赋值运算符大同小异,大家可以试着自行理解,同样也有和上面一样创建临时对象的写法,可以试着理解。
reset(_Ty p)*:该函数是资源覆盖,如果p为nullptr,直接调用无参的reset函数来释放该智能指针对象。如果不为nullptr,则先判断计数器自减之后为否为0来决定是否释放资源,然后使用参数指针来构造新的计数器对象。
循环引用
class Child;
class Parent
{
public:
std::shared_ptr<Child> child;
Parent() { cout << "Parent" << endl; }
~Parent() { cout << "~Parent" << endl; }
void Hi() { cout << "hello Parent" << endl; }
};
class Child
{
public:
std::shared_ptr<Parent> parent;
Child() { cout << "Child" << endl; }
~Child() { cout << "~Child" << endl; }
};
int main()
{
std::shared_ptr<Parent> pa(new Parent());
std::shared_ptr<Child> pc(new Child());
pa->child = pc;
pc->parent = pa;
return 0;
}
运行上面代码我们会发现两个对象都没有析构,这是为什么呢?我们画图来理解:
创建pa对象时,其两个指针分别指向临时的智能指针对象child,计数器对象,而计数器child对象种也有两个指针都为nullptr,然后船舰pc对象时也是样的,接着将pc赋值给pa的智能指针child对象,这样pc对象的计数器智能指针计数器+1,变成了2,然后呢pa也赋值给了pc的智能指针parent对象,导致两者析构的时候都对计数器进行了自减操作,但是自减之后都不为0所以不能析构。而怎么解决这个问题呢?
_Weaks
这就说到了我们的_Weaks弱指针计数器,这时候我们需要在pa和pc类种用弱引用智能指针(weak_ptr),其+1是对_Weaks计数器进行+1,这样就会可以析构对象了,但是有人会说那么弱智能指针的计数器不为0,那怎么办呢?
实际上这两个计数器控制的是两块堆内存,如果智能指针计数器为0,释放指向资源对象的堆内存,两个计数器都为0时释放计数器的堆内存。
int main()
{
std::shared_ptr<Parent> pa(new Parent());
std::shared_ptr<Child> pc(new Child());
pa->child = pc;
pc->parent = pa;
pc->parent.lock()->Hi();
return 0;
}
weak_ptr没有重载operator->和operator*操作符,因此不可以直接通过weak_ptr对象,典型的用法是调用lock函数来获得shared_ptr实例,进而访问对象。
在用了弱引用智能指针怎么调用成员函数呢?
我们发现上面代码中调用了一个lock()函数,因为弱引用智能指针没有重载->和解引用,为此通过调用lock函数获得shared_ptr,判断其共享型引用计数器是否为0来判断是否可以访问原始对象。
初始化智能指针的方法
初始化有下面两种方法:
int main()
{
std::shared_ptr<int> ip(new int(10));//A
map<string, std::shared_ptr<int>> it;
it["xxin"] = std::make_shared<int>(new int(200));//B
it["lxin"] = std::make_shared<int>(new int(100));
return 0;
}
这两种方法呢也各有不同,B的速度比A块,但是呢也有坏处,为什么呢?
我们知道智能指针在堆区申请了两块空间,是两次申请的,而每一次申请都很费时间,而B方法申请是一次性申请够两块内存,比A方法申请两次效率高。我们知道在智能指针计数器为0时释放资源对象,弱引用智能指针计数器也为0时释放计数器,很明显有时这两块资源不能一起释放,当资源对象内存较小时可以等待弱引用智能指针计数器为0一起释放,但当内存很大时等待其结束释放便时很效率很低的。(一次申请的堆内存智能一次释放完,不能分多次释放)。