目录
前言
在上一篇异常的文章中我们提及到了利用RAII思想去解决内存泄露问题,而智能指针就是RAII思想的产物,智能指针到底智能在什么地方呢?下面我们一起来学习吧!!
一、为什么需要智能指针
为什么需要智能指针呢?我们来回顾一下异常章节出现的场景:
#include <iostream>
using namespace std;
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = nullptr;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e) // C++库中提供的可以接收任意类型的基类
{
cout << e.what() << endl;
}
return 0;
}
上述三个问题该如何解决?我们利用重新抛出就能解决上述的问题,那么该如何来写呢?
/*
思路:
如果p1未申请成功,抛出异常直接被主函数捕获返回主函数;
如果p1申请成功,而p2未申请成功,则需在异常被主函数捕获前delete p1,再重新抛出异常交给主函数处理。
此时p1与p2都申请成功,div抛出异常则需在异常被主函数捕获前delete p1 p2,再重新抛出异常交给主函数处理。
*/
#include <iostream>
using namespace std;
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = nullptr;
try
{
p2 = new int;
}
catch (...)
{
delete p1;
throw;
}
try
{
cout << div() << endl;
}
catch (...)
{
delete p1;
delete p2;
throw;
}
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e) // C++库中提供的对任意类型接收的基类
{
cout << e.what() << endl;
}
return 0;
}
我们可以看到问题虽然是解决了,但是设计出的代码确是如此的丑陋,那么对于内存申请与释放问题能不能交给编译器去处理呢?这样就不用我们时时刻刻去操心内存泄露的问题了——智能指针!!
二、智能指针的使用和原理
2.1 RAII思想
RAII
(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
接下来我们基于RAII思想来简单设计一个SmartPtr类,实现对象来管理资源:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{
}
~SmartPtr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
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()
{
// div抛异常, 主函数会捕获到异常, 此时sp1与sp2对象出了Func函数作用域会自动调用析构函数完成资源的清理工作
SmartPtr<int> sp1(new int(1));
SmartPtr<int> sp2(new int(2));
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
2.2 智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* 、->
重载下,才可让其像指针一样去使用。
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{
}
~SmartPtr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
智能指针的原理总结: 利用 RAII 思想设计释放资源的类,该类重载了*
和->
,其具有像指针一样的行为。
如果我们想把这个类的功能再丰富一下,比如说让其可以拷贝构造、赋值重载,我们来看看下面的情况:
我们未重写拷贝构造,那么默认拷贝构造就是浅拷贝,两个对象的成员都指向了同一份资源,那么析构时对同一块空间delete两次就会出现不确定的行为此时程序崩溃,那么按照我们之前所说的浅拷贝不行就换深拷贝分别指向不同块,然后将数据拷贝过来吗?显然在智能指针这里深拷贝是不行的,因为智能指针就需要浅拷贝,使两个对象的成员指向同一份资源!!智能指针拷贝见下图:
那么如何才能实现智能指针的浅拷贝?使得在析构时不对同一份资源析构两次呢?早期C++库中就提供了几种智能指针的设计方案,下面我们一起来看看它们的设计原理。
2.3 std::auto_ptr
C++98版本的库中就提供了auto_ptr
的智能指针。auto_ptr的实现原理:管理权转移的思想。
下面我们根据库中的设计简化模拟实现了一份curry::auto_ptr来了解它的原理:
namespace curry
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr