目录
一、为什么需要智能指针
我们知道在C++中,需要我们程序员手动管理内存,一旦某些地方没有合理的管理内存空间,就会导致内存泄露,造成程序越来越卡直至崩溃的现象。
在C++98之前,没有异常的概念,通常代码写起来比较方便管理内存问题,但是在有了异常之后,某些偶发情况导致异常的产生就会使程序跳转到catch处,同时自己栈帧的后续代码就不会执行了,可能这里面就包含了析构函数,从而导致了内存泄露。
为了更好的解决内存泄漏的问题,C++引入了智能指针。智能指针通俗来讲就是一个类对象,这个类对象封装了我们原本要手动管理的内存指针,借用该对象的构造函数和析构函数,让内存指针无论是正常退出还是异常情况,只要生命周期结束了,就会被销毁。
(比如像下面的代码,每写一行代码都需要考虑一下会不会抛异常)
内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
(下面是常见的导致内存泄露的问题代码)
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何避免内存泄漏
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
2. 采用RAII思想或者智能指针来管理资源。3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
二、智能指针的原理及使用
(1)RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术。通常在智能指针和锁守卫中运用的比较多。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
RAII是一种思想,智能指针就是利用了这一种思想实现的。
(2)auto_ptr
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理:
但是auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr。因为其无法拷贝性导致了使用上的失败。
(2)unique_ptr
因为auto_ptr的失败,C++推出了新的智能指针,专门用来防止拷贝
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理:
// 原理:简单粗暴 -- 防拷贝
namespace bit
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
}
(3)shared_ptr
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共
享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减
一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了
namespace hmy
{
template<class T>
class shared_ptr
{
public:
//构造
shared_ptr(T* ptr = nullptr);
//析构
~shared_ptr();
//拷贝构造
shared_ptr(const shared_ptr<T>& sp);
//赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp);
//获取引用计数
int get_count();
//指针的行为
T& operator*();
T* operator->();
//返回原生指针
T* get_ptr()const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
}
template<class T>
T& hmy::shared_ptr<T>::operator*()
{
return *_ptr;
}
template<class T>
T* hmy::shared_ptr<T>::operator->()
{
return _ptr;
}
template<class T>
hmy::shared_ptr<T>& hmy::shared_ptr<T>::operator=(const shared_ptr<T>& sp)
{
//判断是否是自己给自己赋值
if (_ptr!=sp._ptr)
{
//删除原本的
this->~shared_ptr();
//指向后来的
_ptr = sp._ptr;
_pcount = sp._pcount;
//加加后来的count
*(_pcount)++;
}
//返回*this
return *this;
}
template<class T>
int hmy::shared_ptr<T>::get_count()
{
return *(_pcount);
}
template<class T>
hmy::shared_ptr<T>::shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
template<class T>
hmy::shared_ptr<T>::shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
{
cout << "构造" << endl;
}
template<class T>
hmy::shared_ptr<T>::~shared_ptr()
{
//如果引用计数不为1,就--
if (*_pcount != 1)
{
(*_pcount)--;
}
//如果为1,就delete
else
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
(4)weak_ptr
上面的shared_ptr已经是一个十分好用的智能指针了,但是有时候也会出问题,比如循环引用导致引用计数多于实际数量,从而使得没有正确析构的内存泄露问题。比如下面的代码:
我们来分析一下这是为什么?
1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动
delete。
2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上
一个节点。
4. 也就是说_next析构了,node2就释放了。
5. 也就是说_prev析构了,node1就释放了。
6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和
_prev不会增加node1和node2的引用计数。
namespace hmy
{
// 简化版本的weak_ptr实现
//weak_ptr其实只用封装一下shared_ptr,然后把构造,拷贝构造等的引用计数不++即可
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{
}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get_ptr())
{
}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get_ptr();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
(5)delete时候的问题
在上面的代码中,我们都是使用delete来删除一个指针的,但是如果我们new出来的对象是一个数组呢?此时就需要用到delete[ ]来删除,如何来规避这个写死的问题呢?我们可以在析构函数中传入一个可执行对象,释放的时候不要直接使用delete或者是delete[ ]而是用这个对象来析构。
// 仿函数的删除器
template<class T>
struct FreeFunc
{
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc
{
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
或者是在使用的时候直接传一个lambda表达式
std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; });
std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p){fclose(p); });