智能指针
一、智能指针的使用及原理
1.RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处:
不需要显式地释放资源
采用这种方式,对象所需的资源在其生命期内始终保持有效
智能指针的实现就是运用到了种指导思想,在申请资源的是进行对象构造,把申请到的资源托付给对象进行管理,然后在对象生命周期结束的时析构对象,进行资源释放。
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
ShardPtr<int> sp1(new int);
ShardPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch(const exception& e)
{
cout<<e.what()<<endl;
}
return 0;
}
在上面的代码中,我们就用到了RAII的指导思想,在sp1,sp2对象的生命周期结束的时候,就会自动调用析构函数进行资源的释放。但是上面的SmartPtr还不是一个完整的智能指针,它还没有实现像指针的一样使用和拷贝等众多问题。
2.智能指针的原理
智能指针就是运用RAII的指导思想,实现了一个可以像指针一样使用的东西,就好比迭代器,它也是c++中实现的一种使用起来像指针的东西,只不过智能指针是进行资源管理的,而迭代器是用来遍历非原生指针的容器的(list,map,unordered_map等),所以我们要重载operator* ,operator-> 才可以像指针的一样正常使用 。
再写拷贝构造的时候,我们是否需要自己写一个深拷贝还是用编译器默认生产浅拷贝呢???
首先,我们要明确拷贝的目的是为了一起管理申请出来的这块资源,所以我们要浅拷贝。
template <class T>
class SmartPtr
{
public:
SmartPtr(const T* ptr = nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
#include"Smart.h"
using namespace std;
int main()
{
bit::SmartPtr<int> sp1(new int);
bit::SmartPtr<int> sp2(sp1);
return 0;
}
执行上面代码,结果如下:
报错显示:double free or corruption,我们分析一下sp1 ,sp2都管理的同一块空间,所以在sp1,sp2析构的时候,会对同一块空间释放两次,引起报错。
那么问题来了,我们即想要浅拷贝还想要多个对象管理同一块空间的析构的时候不释放多次资源,我们改如何解决???这个是智能指针的大问题!!!
c++98和c++11都给出了解决方式,下面我们讲解。
3.c++98中的auto_ptr
c++98提出的auto_ptr解决拷贝问题的核心原理就是管理权转移 ,什么是管理权转移呢?让我看一看库里的auto_ptr
#include<iostream>
#include<memory>
using namespace std;
int main()
{
auto_ptr<int> sp1(new int);
auto_ptr<int> sp2(sp1);
return 0;
}
在调试过程中,我们可以清楚的看出sp1把资源转移到sp2中,sp1的管理的资源(_M_ptr)为空指针了,如果我们在不知到把一个对象的管理权转移给了另一个对象的时候,去使用这个对象,就会造成野指针的使用。所以auto_ptr是一个比较失败的设计,很多公司明确说明禁止使用这个智能指针,而且在Linux下,也被抛弃了。
简单的手写一个auto_ptr:
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if(_ptr)
delete _ptr;
}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
4.c++11中的unique_ptr
相比于auto_ptr,unique_ptr不存在了野指针(由管理权转移引起的)访问了,unique_ptr的设计核心原理是禁止发生拷贝和赋值,一旦发生拷贝和赋值就会报编译错误。那么问题来了怎么实现呢???
c++98的解决方式:只声明不实现而且声明为私有
c++11的解决方式:利用关键字delete强制编译器不生成拷贝构造函数和赋值重载函数
上代码:
namespace MySmart_Ptr
{
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
unique_ptr(unique_ptr<int>& sp) = delete;
T& operator=(unique_ptr<int>& sp) = delete;
~unique_ptr()
{
if(_ptr)
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
#include"Smart.h"
int main()
{
MySmart_Ptr::unique_ptr<int> sp1(new int);
MySmart_Ptr::unique_ptr<int> sp2(new int);
MySmart_Ptr::unique_ptr<int> sp3(sp1);
MySmart_Ptr::unique_ptr<int> sp4;
sp4 = sp2;
return 0;
}
运行结果如下:
如我们所愿,当使用了拷贝构造函数和赋值重载函数时,在编译的时候就报错。不使用这两个函数的时候,就可正常的使用了。
5.c++11中的shared_ptr
shared_ptr就不同于unique_ptr了,它是可以使用拷贝构造函数和赋值重载函数的,它的核心原理是运用了引用计数。当发生拷贝和赋值的时候,计数就加一,对象析构的时候只有当计数为一的时候才可以释放资源。如果不理解的话我们就实例演示,上代码:
namespace MySmart_Ptr
{
template <class T>
class shared_ptr
{
private:
void release()
{
if(_ptr)
{
delete _ptr;
_ptr = nullptr;
}
delete _count;
_count = nullptr;
}
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_count(new int(1))
{}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_count(sp._count)
{
(*_count)++;
}
~shared_ptr()
{
if(0 == --(*_count) && _ptr)
release();
}
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if(_ptr != sp._ptr)
{
if(0 == --(*_count) && _ptr)
{
//释放资源
release();
}
_ptr = sp._ptr;
_count = sp._count;
(*_count)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_count;
}
private:
T* _ptr;
int* _count;
//引用计数,初始话的时候new int(1)给对象,发生拷贝和赋值的时候连同它一起拷贝过去并且+1
//这样就会指向同一块空间的多个对象使用同一个计数器,每个对象(指向同一块空间)析构的时候计数器-1,
//直到计数器为零的时候释放空间
};
}
#include"Smart.h"
int main()
{
MySmart_Ptr::shared_ptr<int> sp1(new int);
MySmart_Ptr::shared_ptr<int> sp2(new int);
MySmart_Ptr::shared_ptr<int> sp3(sp1);
MySmart_Ptr::shared_ptr<int> sp4;
sp4 = sp2;
return 0;
}
上面代码执行结果:
其中sp1和sp3一起管理同一块空间,sp2和sp4管理同一块空间。四个对象的析构顺序为sp4,sp3,sp2,sp1。sp4析构_count指向的内容-1不为0,所以不释放空间,而当sp2析构的时候,_count指向的内容-1为0,所以释放空间。sp1和sp3同样如此。
6.c++11中的weak_ptr
weak_ptr是唯一一个没有运用RAII指导思想的智能指针,因为它就不参与资源管理,不会释放资源(有点像迭代器,并不参与资源管理),它不接受指针去初始化它,只接受shared_ptr和weak_ptr去拷贝构造自己。它的存在是专门辅助解决shared_ptr的循环引用的问题。
还有就是weak_ptr并没有重载operator*
和operator->,所以我们不可以像指针一样的使用它,我个人感觉他就像一个没有重载operator*
和operator-> 的迭代器。
#include"Smart.h"
struct ListNode
{
MySmart_Ptr::shared_ptr<ListNode> _next;
MySmart_Ptr::shared_ptr<ListNode> _prev;
int val = 0;
~ListNode()
{
std::cout << "~ListNode" << std::endl;
}
};
int main()
{
MySmart_Ptr::shared_ptr<ListNode> sp1(new ListNode);
MySmart_Ptr::shared_ptr<ListNode> sp2(new ListNode);
sp1->_next = sp2;
sp2->_prev = sp1;
return 0;
}
执行上面代码,结果如下:
我们可以看到,我们申请资源并没有释放,我们分析一下原因:
一开是这两个空间都是有两个对象进行管理的,所以它们的计数器都为2,当sp1和sp2的生命周期到了析构sp1和sp2,由于还有对象管理这两块空间,所以这两块空间不会被释放。右边资源有左边的
_ next管理,左边的资源被右边的_ prev管理。这样一来,左边的资源想要释放需要右边的 _prev析构,而 _prev析构需要右边的资源释放,右边资源释放需要左边的 _next析构,而
_next析构需要左边的资源释放。这样会一直循环下去,一直析构不了对象,一直释放不了支援。我们把这个现象就叫做循环引用问题。weak_ptr就是为了解决这个问题:它不进行资源管理,也不会引用计数,把shared_ptr拷贝或者赋值给它,计数器不会++,所以sp1,sp2生命周期一结束,计数器就会减为0,自然就释放了申请的资源。
namespace MySmart_Ptr
{
template <class T>
class weak_ptr
{
public:
weak_ptr() = default;
weak_ptr(shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(shared_ptr<T>& sp)
{
if(_ptr != sp.get())
{
_ptr = sp.get();
}
return *this;
}
private:
T* _ptr = nullptr;
};
}
#include"Smart.h"
struct ListNode
{
//MySmart_Ptr::shared_ptr<ListNode> _next;
//MySmart_Ptr::shared_ptr<ListNode> _prev;
MySmart_Ptr::weak_ptr<ListNode> _next;
MySmart_Ptr::weak_ptr<ListNode> _prev;
int val = 0;
~ListNode()
{
std::cout << "~ListNode" << std::endl;
}
};
int main()
{
MySmart_Ptr::shared_ptr<ListNode> sp1(new ListNode);
MySmart_Ptr::shared_ptr<ListNode> sp2(new ListNode);
sp1->_next = sp2;
sp2->_prev = sp1;
return 0;
}
执行上面代码,结果如下:
显然,申请的资源被释放了。
二、有关delete[ ]的解决方法
c++中智能指针的默认释放资源用的是delete,如果申请的资源是new[ ],我们需要手写一个定制删除器,也就是通过仿函数进行解决的,在仿函数内delete[ ]。
比如拿unique_ptr举例:
{
template<class T>
struct default_delete
{
void operator()(T* ptr)
{
delete ptr;
}
};
template <class T, class D = default_delete<T>>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
unique_ptr(unique_ptr<int>& sp) = delete;
T& operator=(unique_ptr<int>& sp) = delete;
~unique_ptr()
{
if(_ptr)
//delete _ptr;
D()(_ptr);//匿名对象调operator()
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
#include"Smart.h"
#include<iostream>
using namespace std;
class Date
{
public:
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main()
{
MySmart_Ptr::unique_ptr<Date, DeleteArray<Date>> up2(new Date[10]);
return 0;
}
运行上面代码,执行结果如下: