一、引言
为什么需要智能指针?
在上一篇异常中,关于内存释放,我们提到过一个问题---当我们申请资源之后,由于异常的执行,代码可能直接跳过资源的释放语句到达catch,从而造成内存的泄露,对于这种情况,我们当时的解决方案是在抛出异常后,我们先对异常进行捕获,将资源释放,再将异常抛出,但这样做会使得代码变得很冗长,那有没有什么办法能让它自动释放内存资源呢?用智能指针
什么是智能指针?
说到自动释放资源,是不是有点熟悉,我们在学习创建类对象时,就知道当类对象的生命周期结束后,系统会自动调用它的析构函数,完成资源的释放,那么我将指针放入这样一个类对象中,将释放资源的工作交给析构函数,只要该对象生命周期结束,那么就释放该资源,如此就不用在关心资源的释放问题,只要函数栈帧销毁,即该对象被销毁,资源就会自动释放,这就叫智能指针。
智能指针的使用和原理
1.RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
2.具有指针的行为,可以解引用,也可以通过->去访问所指空间中的内容
下面写一个简单的智能指针
namespace zxws
{
template<class T>
class smart_ptr
{
public:
smart_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~smart_ptr()
{
cout << "delete _ptr" << endl;
delete _ptr;
_ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
但是上面这个智能指针有个严重的问题,一旦有两个对象同时指向同一个资源,那么析构函数就会被调用两次,即资源要被释放两次,会报错,如下
二、库中的智能指针
C++官方给出了3个智能指针
1.auto_ptr
auto_ptr:管理权转移的思想,即一个资源只能有一个指针能对它进行管理,其他的指向这一资源的指针均为空,实现如下
namespace zxws
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
//管理权限的转移
auto_ptr(auto_ptr& tmp)
:_ptr(tmp._ptr)
{
tmp._ptr = nullptr;
}
auto_ptr& operator=(auto_ptr& tmp)
{
if (this != &tmp)//注意自己给自己赋值的情况不需要处理,否则会出问题
{
if (_ptr)//释放当前对象中资源
delete _ptr;
//管理权限转移
_ptr = tmp._ptr;
tmp._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
2.unique_ptr
unique_ptr:简单粗暴的防拷贝,即一个指针只能被初始化一次,且只能用不同的资源初始化
实现如下
namespace zxws
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
//将拷贝构造和赋值重载直接ban掉
unique_ptr(const unique_ptr& tmp) = delete;
unique_ptr& operator=(const unique_ptr& tmp) = delete;
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
3.shared_ptr
shared_ptr是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
具体原理如下
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
namespace zxws
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
shared_ptr(const shared_ptr& tmp)
:_ptr(tmp._ptr)
,_pcount(tmp._pcount)
{
(*_pcount)++;
}
shared_ptr& operator=(const shared_ptr& tmp)
{
//这里注意自己给自己赋值的情况!!!
//当引用计数为1时,就会出现将资源释放后,在赋值的尴尬情况
//用this!=&tmp也没用,可能出现两个不同对象指向同一块资源的情况
//所以用资源的地址来判断最准确
if (_ptr != tmp._ptr)
{
release();
_ptr = tmp._ptr;
_pcount = tmp._pcount;
(*_pcount)++;
}
return *this;
}
void release()
{
if (--(*_pcount)==0)
{
delete _ptr;
delete _pcount;
_pcount = nullptr;
_ptr = nullptr;
}
}
~shared_ptr()
{
release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
};
}
那么引用计数,为什么要用指针开辟的空间,而不是成员变量或者静态成员变量?
1、如果是成员变量,那么每一个shared_ptr对象都会有一个_pcount
2、如果是静态成员变量,那么_pcount将属于一个类
两者都不能满足我们的需求
关于shared_ptr还存在一个循环引用的问题,场景如下
当我们将循环链表的两个结点连接起来的时候,就不会释放结点空间,但是只要有一条边没链接就都能释放,为什么???
而只连接一条边,这个闭环就不复存在,所以两个结点都能释放,那如何解决这种情况?
针对这种情况,C++官方设计出了weak_ptr来和shared_ptr搭配使用,也就是说weak_ptr不增加shared_ptr的引用计数,且不参与资源的释放
实现如下
namespace zxws
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& tmp)
:_ptr(tmp.get())
{}
weak_ptr& operator=(const shared_ptr<T>& tmp)
{
_ptr = tmp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
(上面三个智能指针的模拟实现是被简化过的,功能不全,但是核心就是这些)
其中auto_ptr这个智能指针基本不用
上面写的三个智能指针还有一个缺陷,就是释放资源的delete写死了,如果我们开的是一个数组,就需要用delete[],否则资源的释放就会出现问题,所以就需要我们定制化它们的释放资源的方式,根据前面的知识,我们可以给它传一个释放资源的仿函数,如下
template<class T>
struct Destroy {
void operator()(T*_ptr){
delete[] _ptr;
}
};
template<class T, class D>
class shared_ptr
{
//....
};
shared_ptr<int, Destroy<int>>p;
但是库中只写了一个模板参数
我们如果想实现和库中一样的效果,该怎么写?
既然传模板参数不行,我们只能传函数对象了,用function包装器和lambda表达式实现如下
namespace zxws
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
shared_ptr(T* ptr,function<void(T*)> del)
:_ptr(ptr)
,_pcount(new int(1))
,_del(del)
{}
shared_ptr(const shared_ptr& tmp)
:_ptr(tmp._ptr)
,_pcount(tmp._pcount)
,_del(tmp._del)
{
(*_pcount)++;
}
shared_ptr& operator=(const shared_ptr& tmp)
{
//这里注意自己给自己赋值的情况!!!
//当引用计数为1时,就会出现将资源释放后,在赋值的尴尬情况
//用this!=&tmp也没用,可能出现两个不同对象指向同一块资源的情况
//所以用资源的地址来判断最准确
if (_ptr != tmp._ptr)
{
release();
_ptr = tmp._ptr;
_pcount = tmp._pcount;
_del = tmp._del;
(*_pcount)++;
}
return *this;
}
void release()
{
if (--(*_pcount)==0)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
~shared_ptr()
{
release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)>_del = [](T* ptr) {delete ptr; };
};
}
其他几个智能指针写法类似,就不写了。
4.shared_ptr的线程安全问题
上面我们模拟实现的shared_ptr其实会有线程安全问题,即当多个线程同时进行拷贝构造时,资源的引用计数就有可能出现错误,导致资源不释放/提前释放,测试代码如下
int main()
{
zxws::shared_ptr<int> p = new int(10);
thread t1([&]() {
for (int i = 0; i < 10000; i++) {
zxws::shared_ptr<int> tmp(p);
}
});
thread t2([&]() {
for (int i = 0; i < 10000; i++) {
zxws::shared_ptr<int> tmp(p);
}
});
t1.join();
t2.join();
//cout << p.use_count() << endl;
return 0;
}
所以我们需要给它锁 / 将引用计数的++操作变为原子操作
namespace zxws{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new atomic<int>(1))
{}
shared_ptr(T* ptr,function<void(T*)> del)
:_ptr(ptr)
,_pcount(new int(1))
,_del(del)
{}
shared_ptr(const shared_ptr& tmp)
:_ptr(tmp._ptr)
,_pcount(tmp._pcount)
,_del(tmp._del)
{
++(*_pcount);
}
shared_ptr& operator=(const shared_ptr& tmp)
{
//这里注意自己给自己赋值的情况!!!
//当引用计数为1时,就会出现将资源释放后,在赋值的尴尬情况
//用this!=&tmp也没用,可能出现两个不同对象指向同一块资源的情况
//所以用资源的地址来判断最准确
if (_ptr != tmp._ptr)
{
release();
_ptr = tmp._ptr;
_pcount = tmp._pcount;
_del = tmp._del;
(*_pcount)++;
}
return *this;
}
void release()
{
if (--(*_pcount)==0)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
~shared_ptr()
{
release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
private:
T* _ptr;
//int* _pcount;
atomic<int>* _pcount;
function<void(T*)>_del = [](T* ptr) { delete ptr; };
};
}
库中的shared_ptr是线程安全的,但是它指向的资源不是,所以当有多线程操作时,需要对资源进行加锁保护