
各位大佬好,我是落羽!一个坚持不断学习进步的学生。 如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步! 也欢迎关注我的blog主页: 落羽的落羽
如果一个程序中手动new了对象,申请了空间资源,然后下面抛出了异常,就会导致申请的资源没有手动释放,造成内存泄露了。我们就需要在捕捉到异常后在catch语句里先delete资源。可是,new本身也可能抛异常的,导致我们处理起来就会很麻烦。智能指针在这样的场景下处理就十分轻松了。
double divide(int a, int b)
{
if(b == 0)
{
throw string("Divide by zero condition!");
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
int* arr = new int[10];
try
{
divide(1, 0);
}
catch(string s)
{
delete[] arr;
cout << s << endl;
}
}RAII是Resource Acquisition Is Initialization的缩写,是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取的动态资源,避免资源泄漏,这里的资源包括内存、文件指针、网络连接、互斥锁等。RAII在获取资源时把资源委托给一个对象,控制对资源的访问,资源在对象的生命周期内始终保持有效。在对象析构的同时释放资源,这样就保障了资源的正常释放,避免资源泄露。对象的生命周期结束后会自动调用析构函数,也就能使资源自动释放,无需我们再手动操作。
智能指针类除了满足上述RAII的思想,还需要方便资源的访问,所以一般还需要重载operator* -> []等运算符:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{ }
~SmartPtr()
{
delete[] _ptr;
cout << "资源已释放" << endl;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* ptr;
};有了这样的智能指针类,在遇到手动申请资源时就可以用一个智能指针来接受了,当智能指针生命周期结束时,资源也会在调用的析构函数中释放。

C++标准库也有自己的智能指针,在<memory>这个头文件下。智能指针有很多种,有各自不同的特点。
auto_ptr是C++98就有的智能指针,它的特点是拷贝时会把被拷贝对象的资源管理权转移给拷贝对象。这个设计很不好,因为它会使被拷贝对象悬空变成野指针,稍不注意就会访问报错。C++11后有了别的智能指针,就不要再使用auto_ptr了!
auto_ptr<string> ap1(new string("xxx"));
//拷贝后,资源管理权限转移,ap1悬空
auto_ptr<string> ap2(ap1);
//此时若访问ap1,就会访问野指针,程序可能会挂掉
//cout << *ap1 << endl;unique_ptr是C++11设计出的一种智能指针,它的特点是不支持拷贝,只支持move移动。如果是不需要拷贝的场景就很推荐使用它。
unique_ptr<string> up1(new string("xxx"));
//不支持拷贝,会报错
//unique_ptr<string> up2(up1);
//可以进行移动,但移动后up1也会悬空,移动需谨慎
unique_ptr<string> up3(move(up1));unique_ptr还支持了operator bool的类型转换,如果智能指针对象是一个空对象(没有管理资源),则返回false,否则返回true。所以我们可以把智能指针给if等语句判断是否为空。
shared_ptr是C++11设计出的一种智能指针,它的特点是支持拷贝,也支持移动。底层是用引用计数的方式实现的。
shared_ptr支持拷贝意味着,一份资源可以同时被多个智能指针管理,引用计数用于记录这份资源有几个“管理者”。有多个管理者时,其中一个智能指针对象进行析构,不会释放这份资源,而使引用计数减一。当引用计数为一时,意味着只有最后一个管理者了,这个智能指针进行析构时,才会真正释放资源。引用计数的方式避免了空间重复释放。share_ptr内也有一个接口use_count(),能返回这个智能指针指向的资源的管理者个数。
shared_ptr<string> sp1(new string("xxx"));
shared_ptr<string> sp2(sp1);
shared_ptr<string> sp3(sp1);
cout << sp3.use_count() << endl;

shared_ptr还支持了operator bool的类型转换,如果智能指针对象是一个空对象(没有管理资源),则返回false,否则返回true。所以我们可以把智能指针给if等语句判断是否为空。
weak_ptr也是C++11设计出的一种智能指针,它和上面两种很不一样,它不支持RAII的设计思路,它不是用于直接管理资源的。weak_ptr的作用只在于解决shared_ptr的一个循环引用导致的内存泄漏问题。具体我们下文再讲
库中的智能指针析构时默认使用delete进行资源释放,这意味着如果不是new出来的资源,智能指针析构时就会崩溃。智能指针其实支持在构造时提供一个删除器,本质是一个可调用对象,用于自定义我们想要的资源释放方式。给了定制的删除器,智能指针析构时就会调用删除器去释放资源。但是unique_ptr和shared_ptr的构造时提供删除器的格式还不太一样,也是设计上的一个小缺点了。
实际使用中,除了new,new[]也是经常使用的。所以unique_ptr和shared_ptr都特化了一种支持new[]的版本,使用如shared_ptr<int[]> sp(new int[10]);这样,就可以管理new[]的资源了。
我们来模拟实现一下几种智能指针,其实思路很简单,只要遵循它们的特点就好。注意unique_ptr和shared_ptr的构造函数需要使用explicit修饰, 防止普通指针隐式类型转换成为智能指针对象。
namespace lydly
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T& ptr)
:_ptr(ptr)
{ }
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
//管理权转移
ap._ptr = nullptr;
}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
{
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T& ptr)
:_ptr(ptr)
{
}
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
//不支持拷贝
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
//支持移动
unique_ptr(unique_ptr<T>&& up)
:_ptr(up._ptr)
{
up._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& up)
{
delete _ptr;
_ptr = up._ptr;
up._ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
operator bool()
{
return _ptr != nullptr;
}
private:
T* _ptr;
};
}至于shared_ptr,需要注意设计它的引用计数机制,指向同一个资源的不同智能指针间需要共用一个引用计数,可以使用指针的方式:
namespace lydly
{
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
{ }
//自己提供删除器的构造函数
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
,_pcount(new int(1))
,_del(del)
{ }
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _del(sp._del)
{
//拷贝后引用计数+1
(*_pcount)++;
}
void release()
{
//先使引用计数-1,再判断此时是否为0,为0则释放资源
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
~shared_ptr()
{
release();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
_del = sp._del;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
operator bool()
{
return _ptr != nullptr;
}
private:
T* _ptr;
int* _pcount; //引用计数
function<void(T*)> _del = [](T* ptr) {delete ptr; }; //删除器,默认提供的是delete版本
};
}shared_ptr在大部分情况下管理资源是非常合适的。但是有一种特殊的场景:
struct ListNode
{
int val;
shared_ptr<ListNode> next = nullptr;
shared_ptr<ListNode> prev = nullptr;
};
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->next = n2;
n2->prev = n1;经过这样的代码,n1、n2的prev指向一块资源,n2、n1的next指向一块资源。

这种场景中,n1和n2要怎么进行析构呢? n1调用析构后,第一个资源的引用计数减为1,资源不释放。n2调用析构后,第二个资源的引用计数减为1,资源不释放。此时就进入了一个局面:第一个资源(的成员)管理着第二个资源,第二个资源(的成员)管理着第一个资源。 第一个资源想要释放,需要第二个资源先释放;第二个资源想要释放,需要第一个资源先释放。 逻辑上进入了死循环,谁都无法释放,这就是循环引用问题。

所以,weak_ptr出手了。把ListNode中的shared_ptr改为weak_ptr就能解决这种问题。weak_ptr不会增加引用计数,next和prev就不参与资源的管理了,成功打破循环引用。
struct ListNode
{
int val;
weak_ptr<ListNode> next = nullptr;
weak_ptr<ListNode> prev = nullptr;
};
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->next = n2;
n2->prev = n1;weak_ptr不支持绑定到资源,只支持绑定到shared_ptr,且不增加shared_ptr的引用计数。weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。
expired()检查它指向的资源是否过期。use_count()也可获取shared_ptr的引用计数。lock()返回⼀个管理资源的shared_ptr(也会使引用计数+1),如果资源已经被释放,则返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
本篇完,感谢阅读!