内存泄露、内存溢出以及解决方法

2015/08/02 C和C++基础

内存泄漏

内存泄露是指程序在运行过程中动态申请的内存空间不再使用后没有及时释放,从而很可能导致应用程序内存无线增长。更广义的内存泄露包括未对系统的资源的及时释放,比如句柄等。内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。

内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况,是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。

内存泄漏分类:

1.常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

2.偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

3.一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅一块内存发生泄漏。比如,在一个Singleton类的构造函数中分配内存,在析构函数中却没有释放该内存。而Singleton类只存在一个实例,所以内存泄漏只会发生一次。

4.隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

内存泄露可能发生的一些场景

(1)程序员常常忽略在所有的分支都加上内存的回收处理

int size = 100;  
char *pointer = new char[size];  
if (!xxxAPI(pointer, size)  
{  
    return;                      //这里返回前没有释放内存
}  
delete[]pointer;  

(2)构造函数中申请空间,忘了在析构函数中释放空间

(3)库函数或者系统API会在内部申请空间,然后返回指针给用户;以strdup为例

char *str;  
str = strdup("hello World!");  

strdup申请了一段空间存储字符串”hello World”,然后返回空间地址,这个时候用户经常会忘记释放str;

内存泄露的检测

1、我们一般在vs环境下通过debug来调试程序,根据debug的信息来判断内存泄漏
2、检查代码于是说先从指针和动态内存入手,查找代码,看看有没野指针,有没内存没有释放。

比如:

a、对象计数

方法:在对象构造时计数++,析构时–,每隔一段时间打印对象的数量

优点:没有性能开销,几乎不占用额外内存。定位结果精确。

缺点:侵入式方法,需修改现有代码,而且对于第三方库、STL容器、脚本泄漏等因无法修改代码而无法定位。

b、重载new和delete

方法:重载new/delete,记录分配点(甚至是调用堆栈),定期打印。

优点:没有看出

缺点:侵入式方法,需将头文件加入到大量源文件的头部,以确保重载的宏能够覆盖所有的new/delete。记录分配点需要加锁(如果你的程序是多线程),而且记录分配要占用大量内存(也是占用的程序内存)。

3、利用内存泄露检测工具

如:使用Deleaker(本文采用vs2005)进行内存泄露检查

如下图所示:

A) Deleak安装后自动集成到VS中,在VS“工具”菜单中会加入一个“Deleaker”菜单项。

B) Deleaker能够对GDI,USER对象以及句柄进行检测,是否及时释放。

C) Deleaker能够检测泄露的内存发生地点,即展示其函数栈;双击能够转到相应的文件;

PS:Deleaker对中文不支持

如果有内存泄露Deleaker会在程序调试完弹出对话框如下图所示:

也可使用Viual Leak detector

使用Deleak方便灵活,除了其对中文路径支持问题,但感觉和vs的集成度并不是很高。

Viual Leak detector安装后,要在VS中设置相应的头文件和库路径,在Debug模式下如果要检测相应源文件的内存泄露,则加上”#include "即可;

这样在检测内存泄露,可以在VS的输出窗口进行输出,感觉和VS的集成度更高,结果如下图所示:

同样能够显示 内存泄露处的 调用栈,并且通过双击也可以跳转到文件的内存泄露行,个人还是比较喜欢这种方式的。

使用crtdbg.h中的api

A) _CrtSetDbgFlag函数

int _CrtSetDbgFlag(  
int newFlag  
);  

这个函数用于控制debug模式下堆管理的分配行为;

在main函数开始处添加:

_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);  
//_CRTDBG_REPORT_FLAG:表示获取当前的标示位  
//_CRTDBG_LEAK_CHECK_DF:表示检测内存泄露  

则如果出现内存泄露Debug结束后,输出框将输出:

{150}表示申请的第150块申请的内存空间;

B) 显示内存泄露所在的文件以及行

能够知道有内存泄露是不够的,更需要的信息是哪里内存泄露了?

我们可以在每个源文件的开头定义写这样一条宏定义:

//根据__FILE___和__LINE__能够确定文件和行  
#define new   new(_NORMAL_BLOCK, __FILE__, __LINE__)    

C) 显示内存泄露处的堆栈

此函数在指定的申请堆区空间次序处(即lBreakAlloc)设置断点;

很喜欢这个函数,这个函数结合”A)”中提到的{150},比如使用方法:

_CrtSetBreakAlloc(150); //则在第150次申请堆空间时候设置断点  

这样就可以看到函数调用栈,从而帮助我们更加精确的定位程序泄露的位置(调用栈可是个好玩意)。 个人感觉这种方式虽然要手动的修改代码,但其功能却比前两个工具的有效,因为能够在程序运行的时候查看调用栈,这就意味着能够调试程序。

展示结果如下图所示(自动在第150次申请堆空间处中断):

内存溢出

内存溢出即用户在对其数据缓冲区操作时,超过了其缓冲区的边界;尤其是对缓冲区写操作时,缓冲区的溢出很可能导致程序的异常。

内存溢出(out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存溢出是指程序要求的内存,超出了系统所能分配的范围,从而发生溢出。 内存溢是指在一个域中输入的数据超过它的要求而且没有对此作出处理引发的数据溢出问题,多余的数据就可以作为指令在计算机上运行。通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。此时软件或游戏就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件或游戏一段时间。

Search

    Post Directory