内存泄漏
分类
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new
等从堆中分配的一块内存,用完后必须通过调用相应的free / delete / delete[]
删掉,假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak; - 系统资源泄漏
指程序使用系统分配的资源,比如套接字、文件描述符、管道等,在使用完之后没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定等;
检测
智能指针原理
RAII
- 概念:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术;
- 原理:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,借此我们实际上把管理一份资源的责任托管给了一个对象,这种做法有两大好处:
- 不需要显式地释放资源;
- 采用这种方式,对象所需的资源在其生命期内始终保持有效;
简单实现
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr(){
if(_ptr)
delete _ptr;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
struct Date{
int _year;
int _month;
int _day;
};
void main(){
SmartPtr<int> sp1(new int);
*sp1 = 10
cout << *sp1 <<endl;
SmartPtr<int> sparray(new Date);
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
SmartPtr<int> sp(new int);
delete sp;
}
auto_ptr
概念
- 自动指针:在这个智能指针中允许指针之间的互相拷贝或赋值,例如:构造了一个对象 A 来管理 *ptr 这片空间,此时利用对象 A 拷贝了一个 B,那么 A 将会失去对 *ptr 这片空间的管理权,转而交给 B 来管理,这样的做法是为了防止多个对象来管理同一片资源,造成多次释放的问题;
- 问题:由于存在管理权转移,所以在进行拷贝或者赋值后,原对象所管理的空间指针就失效了,此时再使用原对象进行操作就会出错,因此实际中很多公司明确规定了不能使用
auto_ptr
;
实现
template<class T>
class AutoPtr{
public:
AutoPtr(T* ptr = NULL)
: _ptr(ptr)
{}
~AutoPtr(){
if(_ptr)
delete _ptr;
}
AutoPtr(AutoPtr<T>& ap)
: _ptr(ap._ptr)
{
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap){
if(this->_ptr != ap._ptr){
if(_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*() {return *_ptr;}
T* operator->() { return _ptr;}
private:
T* _ptr;
};
int main(){
AutoPtr<Date> ap(new Date);
AutoPtr<Date> copy(ap);
return 0;
}
unique_ptr
概念
- 唯一指针:由于上面的
auto_ptr
存在管理权转移的问题,所以在这个指针中直接delete
了拷贝、赋值接口,简单粗暴的解决了管理权的这个问题,但是这又使得这个指针的操作过于简单,不够完善;
实现
template<class T>
class UniquePtr{
public:
UniquePtr(T * ptr = nullptr)
: _ptr(ptr)
{}
~UniquePtr(){
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
UniquePtr(UniquePtr<T> const &);
UniquePtr & operator=(UniquePtr<T> const &);
UniquePtr(UniquePtr<T> const &) = delete;
UniquePtr & operator=(UniquePtr<T> const &) = delete;
private:
T * _ptr;
};
shared_ptr
概念
- 共享指针:
auto_ptr
存在不安全问题,unique_ptr
又过于简单,所以就出现了shared_ptr
,该指针是通过引用计数的方式来实现多个shared_ptr
对象之间共享资源;
shared_ptr
在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享;- 在对象被销毁时 (也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一;
- 如果引用计数是 0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是 0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象所管理的指针就成野指针了;
安全问题
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时 ++ 或 --,这个操作不是原子的,可能会导致引用计数错乱,进而导致资源未释放或者程序崩溃的问题,所以只能指针中引用计数 ++ 或 – 是需要加锁的(或者设置成原子属性),以此来保证引用计数的操作是线程安全的;
- 智能指针管理的对象存放在堆上,两个线程中管理同一片资源的对象同时去访问资源,会导致线程安全问题,这个需要外部加锁来实现;
实现
- 需要注意的一点是,当进行对象之间的赋值时,需要关注原来资源的情况,例如要将 sp2 对象的内容赋值给 sp1,那么就意味着 sp1 不再管理它原来的那片资源,所以需要对原来的那片资源计数减一,如果减一之后为 0,就需要释放原来的那片资源,然后再进行赋值操作;
template <class T>
class SharedPtr{
public:
SharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pRefCount(new int(1))
, _pMutex(new mutex)
{}
~SharedPtr() { Release();}
SharedPtr(const SharedPtr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pMutex(sp._pMutex)
{
AddRefCount();
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp){
if (_ptr != sp._ptr){
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;
AddRefCount();
}
return *this;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
int UseCount() {return *_pRefCount;}
T* Get() { return _ptr; }
void AddRefCount(){
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
}
private:
void Release(){
bool deleteflag = false;
_pMutex.lock();
if (--(*_pRefCount) == 0){
delete _ptr;
delete _pRefCount;
deleteflag = true;
}
_pMutex.unlock();
if(deleteflag == true)
delete _pMutex;
}
private:
int* _pRefCount;
T* _ptr;
mutex* _pMutex;
};
循环引用
问题
- node1 和 node2 两个智能指针对象分别指向 n1、n2 两个资源,此时两个资源的引用计数变成 1;
- 资源 n1 的 _prve 智能指针对象指向 n2,资源 n2 的 _next 指向 n1,两个资源的引用计数变成 2;
- node1 和 node2 析构,n1、n2 两个资源的引用计数减到 1,但是 n1 的 _prve 还指向 n2,n2 的 _next 还指向 n1;
- 而 _next 和 _prve 属于两个资源 n1、n2 的成员,n1 销毁时,_prve 才会析构,n2 销毁时,_next 才会析构;
- 但是两个资源因为有对象进行管理呢,计数至少为 1,所以不会被释放,这样导致的结果就叫循环引用,谁也不会释放;
解决
weak_ptr
:弱引用指针,我们可以把资源中的shared_ptr
修改为weak_ptr
,当weak_ptr
类型的对象指向一片资源时,资源的计数不会加一;
struct ListNode{
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main(){
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
return 0;
}
删除器
- 一片空间可以由
malloc / new / new[]
等多种方式申请创建出来,我们也需要使用对应的方式free / delete / delete[]
等进行析构,但是智能指针内部并不知道用户传入的空间是由什么方式开辟出来的,所以需要我们用户显式的传入释放空间所需的删除器; - 我们可以选择是在显示实例化对象时传入删除器,此时传入的是一个类型,然后在类中创建仿函数对象成员变量来进行删除;也可以在构造函数中传入删除器,此时传入的是一个仿函数对象用来初始化类中的删除器成员变量;
template <class T>
struct DeleteDel{
void operator()(T* ptr){
delete ptr;
}
};
template <class T>
struct FreeDel{
void operator()(T* ptr){
free(ptr);
}
};
template <class T>
struct DeleteArrDel{
void operator()(T* ptr){
delete[] ptr;
}
};
template <class T, class Del = DeleteDel<T>>
class SharedPtr{
public:
SharedPtr(T* ptr = nullptr, Del del = Del())
: _ptr(ptr)
, _pRefCount(new int(1))
, _pMutex(new mutex)
, _del(del)
{}
~SharedPtr() { Release();}
SharedPtr(const SharedPtr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pMutex(sp._pMutex)
, _del(sp._del)
{
AddRefCount();
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp){
if (_ptr != sp._ptr){
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;
AddRefCount();
}
return *this;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
int UseCount() {return *_pRefCount;}
T* Get() { return _ptr; }
void AddRefCount(){
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
}
private:
void Release(){
bool deleteflag = false;
_pMutex.lock();
if (--(*_pRefCount) == 0){
_del(_ptr);
delete _pRefCount;
deleteflag = true;
}
_pMutex.unlock();
if(deleteflag == true)
delete _pMutex);
}
private:
int* _pRefCount;
T* _ptr;
mutex* _pMutex;
Del _del;
};