内存泄漏
内存泄露:指的是由于疏忽或者错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而因为涉及的错误,失去了对该内存的控制,因而造成了内存的浪费。
内存泄漏的危害
长期运行的程序出现内存泄泄漏,影响是比较大的,例如操作系统、后台服务等响应会越来越慢,最终可用内存不足以支撑运行,从而卡死。
如何避免内存泄漏?
事前预防型:
- 1、养成良好的设计规范和编码规范,申请内存空间要去匹配并正确释放。PS:但是C++11增加了异常,如果遇到异常还是可能会出现问题。
- 2、采用RAII思想或者智能指针管理动态申请的内存资源。
事后查错型:
- 3、有些公司内部规范使用内部实现的私有内存管理库。这些库自带内存泄漏检测的功能选项
- 4、出现内存泄露问题,使用内存泄漏工具检测。
事先预防型的第一条,碰到异常出现问题的一个例子:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p1 = new int;
cout << div() << endl; // 异常安全的问题
cout << "delete:" << p1 << endl;
delete p1;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
由于出现异常安全的问题,,并没有执行到这两句代码
cout << "delete:" << p1 << endl; delete p1;
这就导致p1没有被释放掉。
这种情况还是比较简单的,如果出现如下的情况,就不太好处理了
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 = nullptr, *p3 = nullptr;
try
{
p2 = new int;
p3 = new int;
cout << div() << endl; // 异常安全的问题
}
catch (...)
{
}
cout <<"delete:"<<p1 << endl;
delete p1;
delete p2;
delete p3;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
针对异常安全可能会出现的各种内存泄漏情况,RAII的思想被提出。
RAII
RAII(Resource Acquisition Initialization),是一种利用对象生命周期来控制程序资源的简单技术。
在对象构造时获取资源,接着控制对资源的访问在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,实际上把管理一份资源的责任托管给了一个对象,这种做法的好处:
- 无需显示地释放资源
- 采用这种方式,对象所需要的资源在其生命周期内始终保持有效。
我们使用RAII思想设计一个SmartPtr类
namespace ydy
{
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
cout << "~SmartPtr() delete ptr" << endl; //为了测试所加
delete _ptr;
_ptr = nullptr;
}
}
private:
T* _ptr;
};
}
并做如下测试:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p = new int;
ydy::SmartPtr<int> sPtr(p);
cout << div() << endl; // 异常安全的问题
cout << "delete:" << p << endl;
delete p;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
智能指针的原理
上面设计的SmartPtr还不能被称为智能指针,因为它并不具备指针的行为。为了给这个类的对象赋予指针的行为,选择在类内重载*、->
。
namespace ydy
{
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~SmartPtr()
{
if (_ptr)
{
cout << "~SmartPtr() delete ptr" << endl; //为了测试所加
delete _ptr;
_ptr = nullptr;
}
}
private:
T* _ptr;
};
}
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p = new int;
ydy::SmartPtr<int> sPtr(p);
*p = 10;
cout << "p=" << (*p) << endl;
cout << div() << endl; // 异常安全的问题
cout << "delete:" << p << endl;
delete p;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
智能指针原理的总结:
- 采用RAII思想,具有RAII的特性。
- 重载了
*、->
操作符,赋予了其类似于指针的行为。
如果我们给SmartPtr
类增加一个拷贝构造,会出现什么情况?
namespace ydy
{
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
SmartPtr(const SmartPtr<T>& ptr)
:_ptr(ptr._ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~SmartPtr()
{
if (_ptr)
{
cout << "~SmartPtr() delete ptr:" << _ptr << endl; //为了测试所加
delete _ptr;
_ptr = nullptr;
}
}
private:
T* _ptr;
};
}
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p = new int;
ydy::SmartPtr<int> sPtr(p);
ydy::SmartPtr<int> sPtr2(sPtr);
ydy::SmartPtr<int> sPtr3(sPtr2);
cout << div() << endl; // 异常安全的问题
cout << "delete:" << p << endl;
delete p;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
SmartPtr类对象的拷贝,实际上是增加了一个类对象,对同一个指针进行。析构的时候会释放同一个指针多次,所以就出现了上面的问题。C++98版本的库中提供了auto_ptr指针,想用来解决这个问题。
std::auto_ptr
auto_ptr的其他部分原理和SmartPtr
类是一样的。它对于拷贝构造的实现思想:管理权转移的思想,下面是一份auto_ptr
的简单模拟:
//auto_ptr
namespace ydy
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(const auto_ptr<T>& ptr)
:_ptr(ptr._ptr)
{
ptr._ptr = NULL;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~auto_ptr()
{
if (_ptr)
{
cout << "~auto_ptr delete ptr:" << _ptr << endl; //为了测试所加
delete _ptr;
_ptr = nullptr;
}
}
private:
T* _ptr;
};
}
auto_ptr的问题:当发生对象拷贝/赋值后,前面的对象就悬空了,前面的管理对象“被迫退休”。
std::unique_ptr
c++11提供了更加靠谱的unique_ptr
类,点击此处查看unique_ptr的相关文档。
unique_ptr的思想非常地简单粗暴:禁止拷贝对象。
下面是一份auto_ptr
的简单模拟:
//c++11 unique_ptr
namespace ydy
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~unique_ptr()
{
if (_ptr)
{
cout << "~unique_ptr delete ptr:" << _ptr << endl; //为了测试所加
delete _ptr;
_ptr = nullptr;
}
}
private:
//c++98的防止拷贝方式:只声明不实现 + 定义为私有
unique_ptr(const unique_ptr<T>& ptr);
unique_ptr<T> operator=(const unique_ptr<T>& ptr);
//C++11的防拷贝方式:使用关键字delete
unique_ptr(const unique_ptr<T>& ptr) = delete;
unique_ptr<T> operator=(const unique_ptr<T>& ptr) = delete;
private:
T* _ptr;
};
}
std::shared_ptr
禁止拷贝这种方法是简单粗暴的。我们也希望有一种温柔的方法,准许拷贝,转而对指针的释放进行一些限制。c++11提供了更为靠谱并且支持拷贝的shared_ptr
类。
shared_ptr
类的思想:通过引用计数的方式实现多个shared_ptr
共同管理一个资源(指针)。
- 1、
shared_ptr
类内,每一个资源维护着一个计数器,用来记录该资源被几个对象所管理。 - 2、调用析构函数销毁对象时,说明减少了一个对该资源的管理者,对象的引用减一。
- 3、如果引用计数是1,表示自己是最后一个管理该资源的对象,析构时必须释放掉资源。
- 4、如果引用计数大于1,表示还有其他对象在管理该资源,析构时不能释放资源。
shared_ptr
类的简单模拟实现:
//shared_ptr
namespace ydy
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pRefCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
{
++(*_pRefCount);
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~shared_ptr()
{
(*_pRefCount)--;
if ((*_pRefCount) == 0 && _ptr)
{
cout << "~shared_ptr delete ptr:" << _ptr << endl; //为了测试所加
delete _ptr;
_ptr = nullptr;
}
}
private:
T* _ptr;
int* _pRefCount;
};
}
还需要考虑std::shared_ptr的线程安全问题
- 1、类内的引用计数是多个智能指针对象共享的,引用计数的
++和--
操作不是原子的,多线程情况下,可能导致引用计数错乱,从而造成资源未释放或程序崩溃的情况。 - 2、智能指针管理的对象存放在堆上,两个线程同时访问,会导致线程安全的问题。
对象中资源的线程安全不归于智能指针管理,但其引用计数的线程安全必须要归它管理。
// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
namespace bit
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pRefCount(new int(1))
, _pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pmtx(sp._pmtx)
{
AddRef();
}
void Release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pRefCount) == 0 && _ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
void AddRef()
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
int use_count()
{
return *_pRefCount;
}
~shared_ptr()
{
Release();
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx;
};
shared_ptr
类内增加了赋值构造(重载了=
操作符)
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
思路如下:
std::shared_ptr的循环引用
来看以下例子:
//std::shared_ptr的循环引用
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _prev;
std::shared_ptr<ListNode> _next;
~ListNode()
{
std::cout << "~ListNode()" << std::endl;
}
};
int main(void)
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
return 0;
}
出现的问题:
针对shared_ptr类的循环引用问题,提出了weak_ptr。
std::weak_ptr
weak_ptr不是常规意义的智能指针,它没有接收一个原生指针的构造函数,也不符合RAII特性。点击此处查看weak_ptr文档
weak_ptr解决shared_ptr的循环引用采用的思想:部分和shared_ptr是一样的,只是不参与资源释放的管理,也就是不操作引用计数。
//只需要将节点内的shared_ptr指针改为weak_ptr就可解决循环引用
struct ListNode
{
int _data;
std::weak_ptr<ListNode> _prev;
std::weak_ptr<ListNode> _next;
~ListNode()
{
std::cout << "~ListNode()" << std::endl;
}
};
int main(void)
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev = node1;
return 0;
}