内存泄露是指程序在运行过程中动态申请的内存空间不再使用后没有及时释放,从而很可能导致应用程序内存无线增长。更广义的内存泄露包括未对系统的资源的及时释放,比如句柄等。
内存溢出即用户在对其数据缓冲区操作时,超过了其缓冲区的边界;尤其是对缓冲区写操作时,缓冲区的溢出很可能导致程序的异常。
一.内存泄露
“知己知彼,方能百战不殆”,如果我们能够比较清楚的了解在编程的时候哪些情况容易导致内存泄露,通过避免这些糟糕的情况,从提高代码的质量本身出发,来抵御潜在导致内存泄露的发生。
1.1先来看看内存泄露可能发生的一些场景:
(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.2.内存泄露的检测
(1)利用内存泄露检测工具
常用的有 BoundsCheaker、Deleaker、Visual Leak Detector等,工具毕竟熟能生巧,用户选择先自己喜欢的一款去用即可。
BoundsChecker没有找到win7下支持VS2005的破解版,用盗版的伤不起啊。
(2)使用Deleaker(本文采用vs2005)进行内存泄露检查
如下图所示:
A) Deleak安装后自动集成到VS中,在VS“工具”菜单中会加入一个“Deleaker”菜单项。
B) Deleaker能够对GDI,USER对象以及句柄进行检测,是否及时释放。
C) Deleaker能够检测泄露的内存发生地点,即展示其函数栈;双击能够转到相应的文件;
PS:Deleaker对中文不支持
如果有内存泄露Deleaker会在程序调试完弹出对话框如下图所示:
(3)使用Viual Leak detector
使用Deleak方便灵活,除了其对中文路径支持问题,但感觉和vs的集成度并不是很高。
Viual Leak detector安装后,要在VS中设置相应的头文件和库路径,在Debug模式下如果要检测相应源文件的内存泄露,则加上"#include <vld.h>"即可;
这样在检测内存泄露,可以在VS的输出窗口进行输出,感觉和VS的集成度更高,结果如下图所示:
同样能够显示 内存泄露处的 调用栈,并且通过双击也可以跳转到文件的内存泄露行,个人还是比较喜欢这种方式的。
(4)在没有工具的情况下,使用crtdbg.h中的api也是个很棒的选择
在MFC中可以看到在程序退出的时候,输出框内结尾部分输出内存泄露,并且点击可以跳转到内存泄露的代码处。
那么在console程序下呢,当然我们同样可以做到(做那些MFC帮我们完成了的细节);
A) _CrtSetDbgFlag函数
- int _CrtSetDbgFlag(
- int newFlag
- );
(函数详细信息参考:http://msdn.microsoft.com/zh-cn/library/5at7yxcs.aspx)
这个函数用于控制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,在申请的堆区序号为lBreakAlloc处设置一个断点
- long _CrtSetBreakAlloc( long lBreakAlloc );
此函数在指定的申请堆区空间次序处(即lBreakAlloc)设置断点;
很喜欢这个函数,这个函数结合"A)"中提到的{150},比如使用方法:
- _CrtSetBreakAlloc(150); //则在第150次申请堆空间时候设置断点
这样就可以看到函数调用栈,从而帮助我们更加精确的定位程序泄露的位置(调用栈可是个好玩意)。
个人感觉这种方式虽然要手动的修改代码,但其功能却比前两个工具的有效,因为能够在程序运行的时候查看调用栈,这就意味着能够调试程序。
展示结果如下图所示(自动在第150次申请堆空间处中断):
二.内存溢出
2.1 内存溢出导致的异常症状
(2)有可能调试的时候不错,运行的时候出错,而且随机出现,这绝对让人很头疼的问题。
(3)庆幸的是,如果编译后的debug程序,直接运行后,如果出错,可以选择调试程序(如下图所示);
千万别以为麻烦就此可以解决了,进入调试状态后,发现出错的地方根本代码没有任何问题,可见内存溢出是个多么令人讨厌的家伙;
2.2 解决方法
虽然他是那么可恶,但也不要忘了是程序员自己一手创建了出来的。也不要灰心,困难总是有方法去解决的。
(1)等到生病的时候,再去看病,或许已经晚了;最好是提前做好预防准备;
A) 比如在程序中多使用strcpy_s、memcpy_s等具有缓冲区大小检查的函数,去取代strcpy、memcpy等;
B)给工程设置编译选项/WX开启(“将警告视为错误”),严格要求自己,这样很可能避免了不少潜在的bug;
C) 对自己的代码做好单元测试
(2)如果出现了这种难以查找的错误,可以从程序源码着手,查看一些和内存操作相关的函数,比如strcpy、memcpy等。
本人曾经在项目中就遇到用一个项目组成员在使用,strcpy拷贝一个字符串到一个空间不够的内存,从而导致程序异常:
- //拷贝字符串,并且返回新的字符串地址
- char * string_copy(const char *source)
- {
- char *p_string;
- int string_len;
- string_len = strlen(source);
- if(source == NULL)
- {
- p_string = (char *)malloc(2*sizeof(char));
- strcpy(p_string, "");
- }
- else
- { //这里错误 string_len+1
- p_string = (char *)malloc((string_len)*sizeof(char));
- strcpy(p_string, source);
- }
- return p_string;
- }
静态地去检查代码方法比较慢,而且不适用于大工程。
(3)检查工具
幸运的是本人接触了一个代码量较大的工程,不幸的是发生了内存溢出问题,而导致程序异常。而且出现的症状,就是调试不错,运行出错,
而且随机出现,并且内存异常的代码处,代码没有任何问题。这个问题纠结了至少一个月,病极乱投医,但找了一些工具大多用于检查内存泄露的。
最终确定了两个工具:
A)BoudsChecker,除了能够检查内存泄露,也能检查内存溢出问题;可惜的是没有找到Win7 下支持VS2005的破解版本
B)AppVerifier,专门用来检测那些用普通方法检测不出的意想不到的bug(比如内存溢出、错误句柄使用等)。而且AppVerifier使用非常简单,
只需要绑定需要测试的的应用程序,并且勾选测试项后保存,使用VS2005进行调试即可。AppVier:
PS:文中所称的内存溢出,用英文专业术语叫做heap corruption
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出.
以发生的方式来分类,内存泄漏可以分为4类:
1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到