我们知道,在程序中,内存泄漏是一个很严重的问题。之前我们说过,手动开出的空间,只要记得释放资源即可。但在C++推出异常后,这种方式已经不可靠了,因为异常的捕捉会扰乱程序的流,导致我们即使手动释放了,空间也不一定得到释放。如下:
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 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
分析上面的问题,你能发现什么?
这里是有可能出现内存泄漏的,即使我们手动释放资源了:
解决方法:
用异常的重新捕获解决
我们可以在func函数中先对div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p1 = new int;
int* p2 = new int;
try
{
cout << div() << endl;
}
catch (...)
{
delete p1;
delete p2;
throw;
}
delete p1;
delete p2;
}
int main()
{
try
{
func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
可以看到,代码非常冗余,这代码看起来就像是💩做的。
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;
}
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->
去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将*
、->
重载下,才可让其像指针一样去使用。
如下:
template<class T>
class SmartPtr {
public:
//RALL
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
//可以和指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
总结一下智能指针的原理:
operator*和opertaor->
,具有像指针一样的行为。除此之外,我们还需要去解决智能指针对象的拷贝问题,在C++的发展历史上,官方推出了一系列智能指针,它们在对象的拷贝问题解决方式都不同,不同场景适用不同的智能指针。
C++98版本的库中就已经提供了auto_ptr的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。
int main()
{
std::auto_ptr<int> ap1(new int(1));
std::auto_ptr<int> ap2(ap1);
*ap2 = 2;//管理权转移给新的对象
//*ap1 = 3; //被拷贝的对象会被置空,访问就会报错
std::auto_ptr<int> ap3(new int(1));
std::auto_ptr<int> ap4(new int(2));
ap3 = ap4;
return 0;
}
可以看到,被拷贝的对象会悬空,如果不慎调用,程序会出现错误。 因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题,于是很多公司也都明确规定了禁止使用auto_ptr。 所以auto_ptr是比较鸡肋的。
模拟实现auto_ptr
namespace surplus
{
template<class T>
class auto_ptr
{
//auto_ptr核心逻辑
//1. RALL:利用生命周期特性来管理资源
//2. 可以像指针一样使用
public:
//RALL
auto_ptr(T* ptr)
:_ptr(ptr)
{ }
~auto_ptr()
{
delete _ptr;
}
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//支持资源的转移:被拷贝的对象置空
//缺陷:被拷贝对象悬空,若被访问会出问题
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
return *this;
}
private:
T* _ptr;
};
}
unique_ptr通过禁止拷贝的方式解决智能指针的拷贝问题,以简单粗暴的防拷贝方式解决了auto_ptr拷贝不安全的问题,适合不需要拷贝的场景。
std::unique_ptr<int> up1(new int(1));
std::unique_ptr<int> up2(up1);//直接报错
std::unique_ptr<int> up3(new int(1));
std::unique_ptr<int> up4(new int(2));
ap3 = ap4;//直接报错
模拟实现unique_ptr
核心逻辑:
namespace Surplus
{
template<class T>
class unique_ptr
{
//unique_ptr核心逻辑
//1. RALL:利用生命周期特性来管理资源
//2. 可以像指针一样使用
//3. 禁止转移资源解决指针悬空的缺陷
public:
//RALL
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
delete _ptr;
}
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//禁止转移资源
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
}
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr,这也是我们最常用的智能指针。
shared_ptr解决对象的拷贝问题的原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
举个栗子:老师下晚自习之前都会通知,让最后走的学生记得把门锁下。
shared_ptr存在循环引用的缺陷
wake_ptr可以帮助解决缺陷,后文讲解wake_ptr时细说。
模拟实现
核心逻辑:
++
。--
(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++
。--
,如果减为0则需要将该资源释放malloc、new、new[]
,不同方式其释放资源的方式也不同,malloc->free、new->delete、new[]->delete[]
。
function<T>
)。template<class T>
class shared_ptr
{
//shared_ptr核心逻辑
//1. RALL:利用生命周期特性来管理资源
//2. 可以像指针一样使用
//3. 可以共享资源(利用引用计数实现)
//4. 存在循环引用缺陷(用wake_ptr解决)
public:
//RALL
shared_ptr(T* ptr = nullptr)
:_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()
{
if (--(*_pcount) == 0)
{
cout << "delete" << endl;
_del(_ptr);
delete _pcount;
}
}
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//可以共享资源
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr)
return *this;
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
//获取当前对象管理的资源对应的引用计数。
int use_count()const
{
return *_pcount;
}
//获取原生指针
T* get()const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
shared_ptr的缺陷:循环引用问题
shared_ptr的循环引用问题在一些特殊的场景下才会产生,比如定义下面的节点类: 说明:在节点类的析构函数添加打印一条提示语,便于我们观察资源是否释放
//节点类
struct Node
{
int _val;
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
~Node(){ cout << "~Node()" << endl; }
};
int main()
{
shared_ptr<Node>sp1(new Node);
shared_ptr<Node>sp2(new Node);
sp1->_next = sp2;
sp2->_prev = sp1;
//打印计数器
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
return 0;
}
可以看到,程序并没有打印释放资源提示语,内存泄漏了,智能指针失效。
循环引用分析:
到头来谁也不会被释放。
解决方案:在引用计数的场景下,把节点类中的_prev和_next成员的类型改成weak_ptr就可以了
//节点类
struct Node
{
int _val;
weak_ptr<Node> _next;
weak_ptr<Node> _prev;
};
weak_ptr设计出来的目的就是解决shared_ptr的循环引用问题的。 解决原理:让_next和_prev与其他对象共享资源时,不参与引用计数。
模拟实现weak_ptr
template<class T>
class weak_ptr
{
//无RALL
//可以像指针一样使用
//可以与shared_ptr共享资源
//无引用计数,唯一作用是解决shared_ptr的缺陷
public:
weak_ptr()
:_ptr(nullptr)
{ }
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//可以与shared_ptr共享资源
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get)
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr;
};