一、引言
为什么需要智能指针?
在上一篇异常中,关于内存释放,我们提到过一个问题---当我们申请资源之后,由于异常的执行,代码可能直接跳过资源的释放语句到达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(const auto_ptr& tmp):_ptr(tmp._ptr){tmp._ptr = nullptr;}auto_ptr& operator=(const 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; };};
}
其他几个智能指针写法类似,就不写了。