智能指针的原理
RAII 是一种利用对象声明周期来控制程序资源的简单技术,在对象构造时获取资源,接着控制对资源的访问使之在对象的声明周期内始终保持有效,最后在对象析构时释放资源。实际上是将这份资源交给一个对象去管理,让资源能够自动释放。
智能指针的原理就是RAII的方法,保证资源能够被自动释放,其次通过operator*()和operator->()的方法,让对象能够按照指针的方式来运行,然后解决浅拷贝的问题,保证资源不会被释放多次引起代码崩溃。
auto_ptr
auto_ptr 是C++98标准库中提供的智能指针
ap1当中管理了new出来的资源,
当用ap1拷贝构造或用赋值运算符重载构造ap2时,ap1 中管理的对象转移到了ap2上。
auto_ptr解决浅拷贝问题采用的是资源完全转移的方式,当ap1 创建时,用户申请的资源首地址交给ap1,再用ap1拷贝构造或者赋值ap2的时候,会将ap1中_ptr的内容复制给ap2中的ptr,然后将ap1中的ptr置空,
当ap1和ap2 分别管理不同的资源时,用ap2 构造ap1,将ap1自己管理的资源释放掉,ap2中的_ptr的内容拷贝给ap1,此时ap1的指向变成了指向ap2管理的空间,ap2原来的_ptr置为空。此时ap2的资源完全转移。
这样完成了浅拷贝问题的解决,但是有很大的问题,不管是ap1还是ap2,当其管理的资源被完全转移后,再对指针进行解引用这种操作等于解引用空指针,会引发崩溃。
后来的报告中改进了auto_ptr,不再通过资源完全转移的方式而通过转移资源释放权限的方式,新增的成员能够表示是否有权力释放资源,当ap1管理资源时,除了将ap1中的_ptr指向管理的空间,还要将其释放权限改为true,当ap1拷贝构造ap2时,首先让ap1和ap2形成资源共享,然后将资源的释放权限交给ap2,保证了资源只能被一个auto_ptr释放。
但是依然有缺陷存在,比如在一个if语句中,创建了一个对象ap3,然后用ap2拷贝构造了ap3,此时ap3和ap1,ap2共享同一份资源,但是ap2的资源释放权力转移给了ap3,在离开if的大括号时,ap3对象要销毁,此时恰好ap3对资源有释放权力,这份资源随着ap3的销毁被释放了,而ap1和ap2成了野指针。 所以后来标准又进行了更改。
因此标准委员会建议不要使用auto_ptr,
auto_ptr模拟实现
namespace my {
template<class T>
class auto_ptr{
public:
auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _owner(false)
{
if (_ptr)
{
_owner = true;
}
}
~auto_ptr(){
if (_ptr && _owner){
delete _ptr;
_owner = false;
}
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
T* Get(){
return _ptr;
}
auto_ptr(auto_ptr<T>& ap)
: _ptr(ap._ptr)
, _owner(ap._owner)
{
ap._owner = false;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap){
if (this != &ap){
if (_ptr && _owner){
delete _ptr;
}
_ptr = ap._ptr;
_owner = ap._owner;
ap._owner = false;
}
return *this;
}
private:
T* _ptr;
bool _owner;
};
}
unique_ptr
c++11中提供了unique_ptr,其原理与auto_ptr差不多,但是解决浅拷贝的问题采用了更直接的手段,让一份资源只能被一个对象管理,对象之间不能共享,即资源独占的方式
方法就是将拷贝构造和赋值运算符重载函数后加上=delete,表示编译器不会再默认生成。这样如果在外部使用拷贝构造或者赋值运算符重载的方法就会直接报错。是一种暴力解决的方式。
还有一种方式可以让类不生成成默认的拷贝构造函数,就是将两个函数的声明写出来但不定义,并且将权限改为私有的。这样在类外也不能定义。
using namespace std;
template<class T>
class DFDef{
public:
void operator()(T*& ptr){
if (ptr)
{
delete ptr;
ptr = nullptr;
}
}
};
// malloc的资源的释放
template<class T>
class Free{
public:
void operator()(T*& ptr){
if (ptr){
free(ptr);
ptr = nullptr;
}
}
};
// 关闭文件指针
class FClose{
public:
void operator()(FILE*& ptr){
if (ptr){
fclose(ptr);
ptr = nullptr;
}
}
};
namespace my{
// T: 资源中所放数据的类型
// DF: 资源的释放方式
// 定制删除器
template<class T, class DF = DFDef<T>>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~unique_ptr(){
if (_ptr){
DF df;
df(_ptr);
}
}
T& operator*(){
return *_ptr;
}
T* operator->(){
return _ptr;
}
T* Get(){
return _ptr;
}
unique_ptr(const unique_ptr<T, DF>&) = delete;
unique_ptr<T, DF>& operator=(const unique_ptr<T, DF>&) = delete;
private:
T* _ptr;
};
}
shared_ptr
是C++11中给出的,是可以共享资源的智能指针,它采用的是引用计数的方式解决浅拷贝问题。
引用计数是shared_ptr类中自己维护的一个成员,表示当前使用这份资源的对象的个数。在使用sp1拷贝构造sp2的时候,除了让sp1和sp2形成资源共享外,还要将引用计数_pcount加上1,。在释放资源时,首先检测该对象是否有管理资源,然后就要看引用计数,将引用计数减1,此时引用计数为0则可以释放,不为0则不能被释放。
在单线程下,这种方式没有问题,但是多线程下可能存在一些极端情况。
比如,两个线程下的两个智能指针共享的是同一份资源,同时向下执行,两个线程在结束时,需要释放其管理的资源,此时都要调用析构,极端的情况,A线程拿到的引用计数是2最终减去1 B线程同时也拿到的是2 最终减去1,引用计数没有归0,两个线程都以为还有其他对象在使用这份空间,最后结束,资源没有被释放,导致内存泄漏。
所以遇到资源共享的情况下要考虑多线程情况下的安全性,一般的处理方法是加锁,同一个时间点只让一个线程进去,保证不会出现一些极端情况。
循环引用问题:
struct ListNode
{
shared_ptr<ListNode> next;
shared_ptr<ListNode> prev;
int data;
ListNode(int x)
: next(nullptr)
, prev(nullptr)
, data(x)
{
cout << "ListNode(int):" << this << endl;
}
~ListNode(){
cout << "~ListNode():" << this << endl;
}
};
void Test()
{
shared_ptr<ListNode> sp1(new ListNode(10));
shared_ptr<ListNode> sp2(new ListNode(20));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->next = sp2;
sp2->prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
思考这段代码的执行过程,
最终各个指针的指向,在函数执行完后,出函数的作用域前要将sp1和sp2两个智能指针的对象释放掉,此时sp2创建的早先释放,sp2 不再使用资源了,所以将20这个节点的引用计数减1,此时发现这块空间还有被使用,是10这个节点中next的_ptr在使用,因此不能被释放,反过来sp1也是如此,sp1 原来管理的资源还有20这个节点中的prev的_ptr在使用,因此也不能释放,此时sp1 和sp2 都与其所管理的资源断开了连接。两个节点之间存在相互引用。
weak_ptr
C++11中提供了weak_ptr,它不能独立管理资源,只有一个作用就是配合shared_ptr来解决循环引问题。
上面的例子中,只需要将节点中的ptr换成weak_ptr,在标准库中,引用计数维护了两份,
一份是use 一份是weak。
程序在执行时,当一个资源被shared_ptr类型的对象共享时,给use增加1,而被weak_ptr类型的对象共享时,给weak增加1。
销毁时,先销毁sp2,因为sp2是shared_ptr 因此给sp2的use计数减1,此时use计数为0,确定资源可以被释放,再给sp2 的weak减1,因此计数还不能被释放,要释放这份资源,其内部的成员也要被释放,sp2这个节点中的prev是weak_ptr指向的是sp1中的计数,因此要释放sp2中的prev,就需要给sp1中的weak计数减去1,此时内部的资源释放,sp2可以释放。
接下来释放sp1,sp1中的next是weak_ptr,因此给它所指向的计数的weak减1,这次weak变成0,这块引用计数可以释放了,此时sp1内部next指向的内容已经全部被释放,因此sp1也可以释放了,sp1是shared_ptr,直接给use减1,再给weak减去1,之前已经减了一次1,此时weak也是0,所以都可以成功释放了。
最后注意:unique_ptr和shared_ptr都可以管理一段连续的空间,需要定制删除器,但是没有意义,管理连续空间有vector之类的容器。
然后智能指针没有重载下表运算符。