内存泄漏:因为疏忽或错误造成程序未能释放已经不再使用的内存的情况
内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的分类(两种):
堆内存泄漏:程序在执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存。但是在使用完这些内存后,没有对这内存进行释放,造成这部分空间将无法再被使用
系统资源泄漏:指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉
内存泄漏带来的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
面对内存泄漏问题,有小伙伴就会说:只要我养成良好的编码规范,申请的内存空间记着匹配的去释放,这样不就好了吗。
良好的编码习惯是没错,但是一般的工程文件很大,代码很多。即使当下申请了资源,但是编写了一大段代码后,也难免会忘记。就当一个人记忆力很好,每次都能记得自己申请的资源,都去释放了对应的资源。但是,面对异常问题又该如何去解决呢?
来看看这样的例子:
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;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
下面简单分析一下代码:
- 先来看看 Func 函数:p1、p2在堆区申请了两个 int 大小的内存。在不考虑申请资源失败的情况下,接下来就是调用 div() 函数,并且输出打印的内容。之后就是对 p1、p2 申请资源的释放。
- div 函数实现的是除法功能。但是,这里得考虑一个除数不能为零问题。当用户输入的除数为零时,直接进行抛异常
问题来了,异常被抛出,执行的程序就会直接跳转到 catch 来捕获异常,不再按照顺序执行。此刻,p1、p2 申请的资源没有释放,直接引发了内存泄漏问题。
当然,抛异常也可以处理:div函数 抛的异常可以在 Func函数内部就直接进行捕获,处理完 p1、p2资源后再将异常抛出到最外层 main 函数即可:
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; //再次抛出异常
}
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
如果 p1、p2申请资源失败了是不是也要进行抛异常处理,处理后的代码会写成下面这样:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
int* p1 = new int;//p1申请资源失败,不需要处理的其他资源
int* p2 = nullptr;
try
{
p2 = new int;
}
catch (...)
{
delete p1;
throw;//p2申请资源失败,释放p1申请的资源,再抛出异常
}
try
{
cout << div() << endl;
}
catch (...)
{
delete p1;
delete p2;
throw;//div函数执行错误,释放p1、p2申请的资源,再抛出异常
}
}
这里还只是处理两个资源释放的问题,在一个项目中存在着大量的资源申请与释放的操作,难道抛异常都这样去实现吗?
这样操作是不现实的,很繁琐很麻烦!为了解决类似 C++ 抛异常的问题,提出了智能指针方案。
开始前,来了解一下 RAII 思想:
RAII 是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
使用 RAII 思想来实现对资源控制的好处:
RAII 是一种思想,智能指针就是在这样的思想下构建出来的。
接下来,小编就按着 RAII 的思想来模拟实现一个简单版本的智能指针:
定义一个 SmartPtr 模板类,内部只包含一个指针成员变量。构造函数完成对资源的申请、析构函数实现对资源的释放:
template<typename T>
class smartPoint
{
public:
//构造
smartPoint(T* ptr)
:_ptr(ptr)
{
std::cout << "smartPoint():new _ptr" << std::endl;
}
//析构
~smartPoint()
{
std::cout << "~smartPoint():delete _ptr" << std::endl;
delete _ptr;
}
private:
T* _ptr;
};
一个对象的生命周期是从实例化对象开始,到这个对象所处的函数生命周期为结束。
int main()
{
smartPoint<int> p1(new int);
return 0;
}
有了智能指针的这套玩法,在抛异常那我们还需要焦头烂额吗?利用RAII思想就可以很好解决抛异常问题:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
smartPoint<int> p1(new int);
smartPoint<int> p2(new int);
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
异常随便你怎么抛,程序执行跳转前都要先对栈帧资源先清空,对此,智能指针能够很好解决资源的释放问题。
接下来完善一下 SmartPtr 类:
智能指针也是指针,如果想要像指针那般访问到资源数据,就要在类中实现对应的 operator*()
与 operator->()
( operator->()
常用于自定义类型的数据)
template<typename T>
class smartPoint
{
public:
//构造
smartPoint(T* ptr)
:_ptr(ptr)
{
std::cout << "smartPoint():new _ptr" << std::endl;
}
//析构
~smartPoint()
{
std::cout << "~smartPoint():delete _ptr" << std::endl;
delete _ptr;
}
//解引用获取数据资源
T& operator*()
{
return *_ptr;
}
//->用于自定义类型
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
实例化一个 smartPoint 对象,输出对应的数据:
#include
using namespace std;
int main()
{
//RAII:利用对象的生命周期来管理申请的资源
smartPoint<<