思想
在写代码的时候,比如要申请一块内存资源,用完要释放:
int test()
{
T * ptr = new T(...);
do something;
delete ptr;
}
可是有时候,当你在都something的时候出现了异常,或者提前return了,那么这个资源就没法释放。
解决方案
我们知道,对象在离开作用域的时候会自动调用析构函数,所以如果这个资源句柄如果是一个对象就好了,鉴于此,我们可以把句柄包装成一个对象。下面是demo:
template<T>
class Ptr
{
public:
Ptr(T* p):m_ptr(p){};
~Ptr():{delete m_ptr};
T* get_ptr(){return m_ptr};
private:
T* m_ptr;
}
进阶
理论来说上面的方案已经可以解决了,但是我们使用的代码有时候会出现下面的情况:
//原始
{
int* ptr_1 = new int(5);
int* ptr_2 = ptr_1;
do_something(ptr_2);
delete ptr_1;
}
//资源管理
{
Ptr<int> ptr_1 = Ptr(new int(5));
Ptr<int> ptr_2 = ptr_1;
do_something(ptr_2);
//~ptr_2()
//~ptr_1() error
}
可以看到的是,原始代码正常,但是利用资源管理会出现问题,原因就是copy构造后,导致我们的资源被释放两次。
解决方案
shared_ptr是一个经典的内存资源管理类,核心思想就是:统计目前资源句柄被几个管理对象持有,当没有管理对象持有的时候再释放
,根据这个描述,我们就开始盘算,如果这个资源自己有个计数器就好了,我每次能不能释放就看看这个计数器是不是等于0,表示如下:
根据这个思想,我们可以实现如下:
template<T>
class Ptr
{
public:
Ptr(T* p);
Ptr(const Ptr<T>& p);
Ptr<T>& operator=const Ptr<T>& p);
~Ptr();
T* get_ptr(){return m_ptr};
private:
T* m_ptr;
int* m_num_ptr;
}
//第一次的时候,绿色内存要自己申请,毕竟资源自己不会自带计数器
Ptr::Ptr(T* ptr):m_ptr(ptr), m_num_ptr(new int(1)){};
//当拷贝构造的时候,理论来说计数器应该加1,因为资源管理对象又多了一个
Ptr::Ptr(const Ptr<T>& p)m_ptr(p.ptr), m_num_ptr(p.m_num_ptr)
{
(*m_num_ptr)++
};
//到赋值的时候,
Ptr<T>& Ptr::operator= (const SharedPtr&s)
{
if (this!= &s)
{
if(!--(this.m_num_ptr))
{
delete this->m_ptr;
delete this->m_num_ptr;
}
this.m_ptr = s.m_ptr;
this.m_num_ptr = m_num_ptr;
(*m_num_ptr)++;
}
return *this;
}
Ptr::~Ptr()
{
--(*m_num_ptr);//资源计数器减一
if(m_num_ptr==0)
{
delete m_ptr;//把对应的资源释放
m_ptr = nullptr;
delete m_num_ptr;//把自己补的资源也释放
m_num_ptr = nullptr;
}
}
以上实现核心就是在资源的旁边再开一块计数资源,这个int内存和原始资源荣辱与共。
问题
上述存在三个问题:
- 循环引用(shared_ptr是强引用,可以用weak_ptr弱引用解决)
- 多线程(需要自己加lock)
- 不是new实现的内存资源,所以delete失效(定制删除器)
注意
智能指针auto_ptr和shared_ptr两者都在析构函数中做delete,而不是delete [] 动作,所以在做数组的时候要避免,因为shared_ptr pt(new int[1024])不会在编译的时候报错,所以尽量用vector对象。