一,智能指针的使用和原理
1.1,RAII
RAII是基于对象的生命周期来控制程序资源(如内存,网络连接,互斥量)等,在对象构造时获取对象的资源,在对象析构的时候释放资源。实际上就是把管理一个资源责任交给一个对象,这样做的好处是:不需要显式的释放资源,对象在其生命周期内一直有效。
使用这种思想,我们可以模拟实现一个SmartPtr类实现对资源的管理。
#include<iostream>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
};
namespace Etta
{
template<class T>
class SmartPtr
//这种指针无法对对象进行拷贝和赋值。
//如果拷贝构造和赋值,就会重新开空间,实现深拷贝,且对相同资源做了多次管理,
//不可靠且浪费资源,由此引出了auto_ptr
{
public:
SmartPtr(T*ptr=nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
{
delete _ptr;
}
}
private:
T*_ptr;
};
void test()
{
//把对象成员交给类取管理,当出了作用域以后,就对类析构,释放对象资源
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout<<*sp1<<endl;
SmartPtr<Date> sparray(new Date);
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
}
}
1.2,智能指针
上述SmartPtr不能成为智能指针,因为不具有解引用,->功能,所以我们需要重载->和*才能算是一个智能指针。
#include"SmartPtr.h"
// auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
// C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr
namespace Etta
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T*ptr=nullptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if(_ptr)
{
delete _ptr;
}
}
//拷贝构造和赋值、
// 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
// 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
auto_ptr(auto_ptr<T>&ap)
:_ptr(ap._ptr)
{
ap._ptr=nullptr;
}
auto_ptr<T>& operator=(const auto_ptr<T>&ap)
{
if(this!=&ap)
{
if(_ptr)
{
delete _ptr;
}
_ptr=ap._ptr;
ap._ptr=nullptr;
}
return *this;
}
T*operator->()
{
return _ptr;
}
T&operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
void test_auto_ptr()
{
auto_ptr<Date> ap(new Date);
// 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
// 通过ap对象访问资源时就会出现问题。
auto_ptr<Date> copy(ap);
ap->_year = 2018;
}
}
但是auto_ptr的赋值和拷贝构造是通过对资源的管理权转移,导致原指针悬空,所以很多公司禁止了使用这种智能指针,所以衍生出unique_ptr。
智能指针的特性:
RAII特性
具有像指针一样的行为,支持->和*。
1.3,unique_ptr(不支持拷贝和赋值)
unique_ptr的实现原理:简单粗暴的防拷贝
//简单粗暴的防拷贝,简化模拟实现了一份UniquePtr来了解
namespace Etta
{
template<class T>
class unique_ptr
{
public:
// RAII
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
// 可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>operator=(const unique_ptr<T>&) = delete;
private:
T* _ptr;
};
void test_unique_ptr()
{
//std::unique_ptr<int> sp1(new int);
// 拷贝
//std::unique_ptr<int> sp2(sp1);
unique_ptr<int> sp1(new int);
// 拷贝
//bit::unique_ptr<int> sp2(sp1);
}
}
缺点就是不支持拷贝和赋值,为了更加靠谱和支持拷贝构造,C++11提出了shared_ptr
1.4,shared_ptr
1, shared_ptr的原理:
是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了
5,因为存在计数,所以多个线程访问时,容易导致两个线程同时+,本来是3,但实际上变成2,所以为了防止这种情况,需要在拷贝和赋值的时候加锁处理。
模拟实现:
#include<thread>
#include<mutex>
#include<iostream>
using namespace std;
namespace Etta
{
template<class T>
class share_ptr
{
public:
share_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_count(new int(1))
,_mtx(new mutex)
{}
~share_ptr()
{
ReleaseRef();
}
share_ptr(const share_ptr<T>&sp)
:_ptr(sp._ptr)
,_mtx(sp._mtx)
,_count(sp._count)
{
AddRef();
}
//将sp赋值给this
//先减去_ptr的计数,再把sp的空间给给_ptr,再对_ptr加加
share_ptr<int>&operator=(const share_ptr<T>&sp)
{
if(_ptr!=sp._ptr)
{
ReleaseRef();
_ptr=sp._ptr;
_mtx=sp._mtx;
_count=sp._count;
AddRef();
}
return *this;
}
T* get() const
{
return _ptr;
}
T&operator*()
{
return *_ptr;
}
T*operator->()
{
return _ptr;
}
int use_count()
{
return *_count;
}
private:
T *_ptr;
mutex* _mtx;
int *(_count);
void AddRef()
{
//为什么要加锁,因为不加锁,在两个线程同时拷贝一个对象的话,如果都进来++、
//容易导致产生加了2次,但实际上值为2
//产生线程错误
_mtx->lock();
++(*_count);
_mtx->unlock();
}
void ReleaseRef()
{
_mtx->lock();
bool flag=false;
if(--(*_count)==0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _count;
flag=true;
}
_mtx->unlock();
if(flag==true)
{
delete _mtx;
}
}
};
}
2,循环引用问题
先看这个问题:我们使用shared_ptr对节点进行管理。
看看跑起来结果怎么样。
struct ListNode
{
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
int _val;
~ListNode()
{
cout << "~ListNode()"<<endl;
}
};
void test_shared_ptr_cycle()
{
std::share_ptr<ListNode>node1(new ListNode);
std::share_ptr<ListNode>node2(new ListNode);
cout << node1.use_count() <<endl;
cout << node2.use_count() << endl;
node1->_next=node2;
node2->_prev=node1;
cout << node1.use_count() <<endl;
cout << node2.use_count() << endl;
}
可以看到程序会崩掉,在最后没有调用ListNode的析构,导致这两个节点未被释放,
此时,
解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是:node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数
1.5,weak_ptr
不参与资源管理,不增加shared_ptr管理资源的引用计数,可以像指针一样使用。
// 不参与资源管理,不增加shared_ptr管理资源的引用计数,可以像指针一样使用
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
// 可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
二,总结
智能指针主要有以下思想:
RAII: RAII是基于对象的生命周期来控制程序资源,构造的时候管理,析构的时候释放。
shared_ptr:对指针的拷贝和赋值通过一个int*(count)进行++计数,析构--,加锁控制线程安全。
weak_ptr;对象内成员不参与资源的管理,只是作为一个指针使用。