什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定
1.在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具
2.在windows下使用第三方工具:VLD工具说明
3.其他工具:内存泄露检测工具比较
1.工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
2.采用RAII思想或者智能指针来管理资源。
3.有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4.出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
C++没有垃圾回收机制,资源需要自己手动的进行管理,同时,异常会导致执行流乱跳,所以C++异常非常容易导致内存泄漏这种安全问题,我们以下面的代码为例:
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;
cout << div() << endl;
cout << "delete p1" << endl;
cout << "delete p2" << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
上面这段程序最右可能发生内存泄漏的情况是div函数抛异常,导致程序直接跳转到main函数的catch语句处,导致p1和p2指向的空间没有被释放
针对这种情况我们的做法是在Func函数中对div异常进行捕获,将p1和p2进行释放,最后再将异常重新抛出
虽然这样可以达到目的,但是这样的代码显然很不好,而且最重要的是new也可能会抛异常,在上面的程序中,如果p1new空间失败,此时不会发生内存泄漏,但是如果p1new空间成功,而p2new空间失败,那么p1就会发生内存泄漏,此时我们就需要在int* p2 = new int;语句这里继续嵌套一层try-catch语句,那么还有p3,p4呢?为了解决这种问题,C++设计出了智能指针类解决。
智能指针本质上是一个类,这个类的成员函数及其功能被分为两类:
1.RAII:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。它的主要功能如下:
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效
在对象析构的时候释放资源。
借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
也就是说,RAII就是类的构造函数和析构函数,我们将申请到的资源通过构造函数托付给类的对象来管理,然后再类对象销毁的时候调用析构函数时自动释放资源,在构造和析构期间该资源始终有效
2.支持指针的各种行为。它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:智能指针模板类中还得需要将*、->重载下,才可让其像指针一样去使用。
下面是一个简单的智能指针示例:
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
delete _ptr;
cout << "~SmartPtr()" << _ptr << endl;
}
}
T operator[](size_t pos)
{
return _ptr[pos];
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
如上,我们将new出来的资源交给类的局部对象,这样在类对象生命周期内该资源都有效,类对象销毁时该资源也会自动被释放,并且我们也可以像正常使用指针一样通过类对象对资源进行各种操作,以后,当 p1 p2new空间失败或者div函数抛异常时,由于异常发生会正常释放函数的占空间,那么被局部对象管理的资源也能够正常的进行释放,从而很大程度上缓解了异常的内存泄漏问题。
智能指针虽然能够很好的管理资源,但是智能指针的拷贝和赋值是一个很大的问题,它涉及到资源的管理权问题–由谁管理,由一个单独管理还是多个共同管理。
auto_ptr是C++第一个智能指针,它解决智能指针拷贝问题的方式是管理权转移,即当前对象拷贝构造一个新的对象的时候,会将当前对象的资源交给一个新的对象,然后将自己的资源置空,auto_ptr最大的问题就是它会导致对象悬空,即后面再使用当前对象时,会造成空指针的解引用。
由于auto_ptr非常危险,所以很多公司明确规定不能使用它,并且C++11也已经弃用了auto_ptr,并使用unique_ptr来替代它
下面是auto_ptr的简单模拟实现:
template<class T>
class auto_ptr
{
public:
// RAII
// 保存资源
auto_ptr(T* ptr)
:_ptr(ptr)
{}
// 释放资源
~auto_ptr()
{
delete _ptr;
}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (ap._ptr != _ptr)
{
this->~auto_ptr();
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
unique_ptr是C++11提出的一种更安全的智能指针,它解决拷贝问题的方式是直接不允许拷贝–防拷贝
下面是unique_ptr的简单模拟实现:
template<class T>
class unique_ptr
{
public:
// RAII
// 保存资源
unique_ptr(T* ptr)
:_ptr(ptr)
{}
// 释放资源
~unique_ptr()
{
delete _ptr;
}
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
shared_ptr是C++中使用的最多的一个智能指针,它是通过引用计数来解决智能指针的拷贝问题,使得一份资源可以被多个类对象共同管理,同时,share_ptr的引用计数是线程安全的。
shared_ptr使用引用计数的方式来解决拷贝问题,即用当前对象拷贝一个新的对象的时候,我们让新对象与当前对象共同来管理这份资源,并以++引用计数的方式来标识这份资源被多少个对象所管理,当对象销毁时,引用计数–,但是资源不一定会被销毁,只有当引用计数减为0的时候才会释放资源。
对于我们如何设计引用计数,有如下几种方案:
1.在类中增加一个普通的成员变量count作为引用计数,这种做法显然是不行的,因为每个对象都有自己独立的成员变,因此当前对象的引用计数增加并不会影响 也指向当前资源的其他对象中count的值
2.在类中增加一个静态成员变count–这种做法也是不行的,因为静态成员变量属于整个类,也属于类的所有对象,但是当我们创建的对象不是管理同一份资源的时候,计数是不准确的,只能管理一份资源的时候才是正确的。
3.在类中增加一个指针类型的成员变量,该指针指向一块空间,空间中保存的是当前资源对应的引用计数。相当于类对象要管理的资源比之前多了一个count,这样对于管理不同的资源的类对象来说,二者的引用计数是不会互相的影响的。对于管理同一份资源的类来说,引用计数的变化是同步的
总结:
1.shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。 shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2.在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
3.如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
4.如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
下面的shared_ptr的初步实现:
template<class T>
class shared_ptr
{
public:
// RAII
// 保存资源
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
// 释放资源
~shared_ptr()
{
release();
}
void release()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
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)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++_pcount;
}
return *this;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
int* _pcount;
};
我们上面实现的shared_ptr在多线程环境下可能会发生线程安全问题,而库中的shared_ptr 则不会,如下:
void test_shared_ptr1()
{
int n = 50000;
hdp::shared_ptr<int> sp1(new int);
thread t1([&]()
{
for (int i = 0; i < n; ++i)
{
hdp::shared_ptr<int> sp2(sp1);
}
});
thread t2([&]()
{
for (int i = 0; i < n; ++i)
{
hdp::shared_ptr<int> sp3(sp1);
}
});
t1.join();
t2.join();
cout << sp1.use_count() << endl;
cout << sp1.get() << endl;
}
我们可以看到,我们自己实现的shared_ptr在多线程环境下运行后引用计数的值是错误的且是随机的(正确的应该是1),而库中的shared_ptr则是正确的,其原因如下:
1.我们使用当前对象拷贝构造一个新的对象来共同管理当前资源时,资源的引用计数会++,当局部对象出作用域销毁时引用计数会–,但是语言级别的+±-的操作都是非原子的,因为他们都对应着多条汇编指令,而在多线程的环境下,可能只有一条指令执行了汇编指令该线程就被挂起了,即两个线程同时拷贝对象的时候,一个线程++之后还没有返回,另一个线程就++,此时最后的结果就会少加一次,销毁时也是如此。所以此时就可能会引发线程安全的问题。
2.而库中的shared_ptr的引用计数之所以是线程安全的,是因为它使用了互斥锁对引用计数的++和–进行了保护,即通过加锁使得多线程只能串行的修改引用计数的值,不能并行或并发的修改引用计数
3.加锁和解锁的过程是原子的(有特殊的一条汇编指令来完成锁状态的修改),所以锁本身是线程安全的,我们不需要担心锁的安全性
我们可以使用互斥锁的方式来模拟实现shared_ptr,需要注意的是,和引用计数一样,使用互斥锁的方式也是在类中增加一个互斥指针类型的成员变量,该变量指向堆的一块空间,因为我们要保证的是同一份资源中的同一个引用计数只能被多线程串行访问,而不同资源中的两个无关的引用计数是可以并行/并发操作的。
总结:
需要注意的是shared_ptr的线程安全分为两方面:
1.智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
2.智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
代码实现如下:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
{}
~shared_ptr()
{
Release();
}
//void Release()
//{
// // 引用计数为0才进行析构
// //使用互斥锁来保证引用计数只能被线程串性访问
// // 标志为用于判断是否释放锁
// bool flag = false;
// _pmtx->lock();
// if (--(*_pcount) == 0)
// {
// delete _ptr;
// delete _pcount;
// flag = true;
// }
// _pmtx->unlock();
// if (flag == true)
// {
// delete _pmtx;
// }
//
//}
void Release()
{
// 使用互斥锁来保证引用计数只能被线程串行访问
_pmtx->lock();
if (--(*_pcount) == 0)
{
_pmtx->unlock(); // 先释放互斥锁
delete _ptr;
delete _pcount;
delete _pmtx;
return;
}
_pmtx->unlock();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (sp._ptr != _ptr)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
_pmtx->lock();
(*_pcount)++;
_pmtx->unlock();
}
return *this;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx; //互斥锁
};
需要注意的是,shared_ptr的引用计数是安全的,因为有互斥锁的包含,但是shared_ptr的数据资源是不安全的,因为在堆上的资源的访问是人处理的,shared_ptr无法对其进行保护,如下:
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void test_shared_ptr2()
{
int n = 50000;
hdp::shared_ptr<Date> sp1(new Date);
thread t1([&]()
{
for (int i = 0; i < n; ++i)
{
hdp::shared_ptr<Date> sp2(sp1);
sp2->_year++;
sp2->_day++;
sp2->_month++;
}
});
thread t2([&]()
{
for (int i = 0; i < n; ++i)
{
hdp::shared_ptr<Date> sp3(sp1);
sp3->_year++;
sp3->_day++;
sp3->_month++;
}
});
t1.join();
t2.join();
cout << sp1.use_count() << endl;
cout << sp1.get() << endl;
cout << sp1->_year << endl;
cout << sp1->_month << endl;
cout << sp1->_day << endl;
}
大家也可以使用std::shared_ptr来进行测试,结果也是错误的,所以,对于数据资源的安全我们需要手动的对其加锁来进行保护,代码如下:
void test_shared_ptr2()
{
int n = 50000;
mutex mtx;
hdp::shared_ptr<Date> sp1(new Date);
thread t1([&]()
{
for (int i = 0; i < n; ++i)
{
hdp::shared_ptr<Date> sp2(sp1);
mtx.lock();
sp2->_year++;
sp2->_day++;
sp2->_month++;
mtx.unlock();
}
});
thread t2([&]()
{
for (int i = 0; i < n; ++i)
{
hdp::shared_ptr<Date> sp3(sp1);
mtx.lock();
sp3->_year++;
sp3->_day++;
sp3->_month++;
mtx.unlock();
}
});
t1.join();
t2.join();
cout << sp1.use_count() << endl;
cout << sp1.get() << endl;
cout << sp1->_year << endl;
cout << sp1->_month << endl;
cout << sp1->_day << endl;
}
shared_ptr在绝大多数的情况下都是没有问题的,但是它在一些特殊的场景下就会存在一定的缺陷,如下:
struct ListNode
{
int _data;
ListNode* _prev;
ListNode* _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr4()
{
ListNode* n1 = new ListNode;
ListNode* n2 = new ListNode;
n1->_next = n2;
n2->_prev = n1;
delete n1;
delete n2;
}
在没有智能指针对于new出来的节点我们需要手动delete,但是有了智能指针之后,我们就可以将节点的资源交给智能指针对象来进行管理,这里需要注意的是,节点内部的指针我们也需要使用智能指针,否则就会存在将自定义类型赋值给内置类型,出现类型不匹配而报错:
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr5()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
/*node1->_next = node2;
node2->_prev = node1;*/
}
但是我们发现,当我们让n1的next指向n2,n2的prev指向n1后,程序发生了内存泄漏:
这是因为在当前场景下发生了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成员,所以这就叫循环引用,谁也不会释放。
所以两个节点就会相互等待对方释放,从而满足自身释放的条件。为了弥补shared_ptr的缺陷,C++设计出了weak_ptr来解决shared_ptr的循环引用问题。
weak_ptr是为了解决shared_ptr的循环引用问题而专门设计的智能指针,weak_ptr解决循环引用的方式很简单-不增加资源的引用计数,所以它需要程序员在合适的地方使用它。
weak_ptr的简单模拟实现如下:
template<class T>
class weak_ptr
{
public:
weak_ptr(T* ptr = nullptr)
: _ptr(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;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
前面我们都是一次申请一份资源,即new int 之类的,所以我们的析构可以直接使用delete,但是如果一次申请多份资源,比如new int[10],此时我们释放时就需要使用delete[]了,否则就会出现程序崩溃:
C++通过定制删除器来解决delete和delete[]的问题,定制删除器本质上是一个仿函数/函数对象.
C++标准库中定义的shared_ptr允许我们将函数对象作为构造函数的参数来进行传递,这是因为shared_ptr必须通过引用计数的方式来管理锁指向的资源,对于一个shared_ptr对象来说,它所管理的资源是由其内部包含的指针(ptr&&pcount&&pmtx)和对应的删除器共同管理的,当最后一个shared_ptr对象被销毁时,就会调用删除器来释放所指向的内存,所以shared_ptr底层实现中是一个类专门来管理引用计数和删除器的:
但是对于其他不需要引用计数的智能指针来说,就只能通过模板参数来传递仿函数进行定制删除了,只是模板参数只能类型传递,而不能传递函数对象,所以就无法配合lambda表达式或者包装器对象进行使用。
我们可以对我们模拟实现的shared_ptr进行改造,这里我们就将其改造为通过模板参数来传递仿函数进行定制删除的版本,而不实现支持通过构造函数传递函数对象进行定制删除的版本,代码如下:
template<class T>
class default_delete
{
public:
void operator()(T* ptr)
{
delete ptr;
}
};
template<class T, class D = default_delete<T>>
class shared_ptr
{
public:
// RAII
// 保存资源
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
{}
// 释放资源
~shared_ptr()
{
Release();
}
// sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{
_pmtx->lock(); // t1 t2
++(*_pcount);
_pmtx->unlock();
}
void Release()
{
bool flag = false;
_pmtx->lock();
if (--(*_pcount) == 0)
{
//delete _ptr;
_del(_ptr);
delete _pcount;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
// sp1 = sp1;
// sp1 = sp2;
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_pcount = sp._pcount;
_ptr = sp._ptr;
_pmtx = sp._pmtx;
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
return *this;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx;
D _del;
};
C++11和boost中智能指针的关系
1.C++ 98 中产生了第一个智能指针auto_ptr.
2.C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
3.C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
4.C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。