摘要:本文简单介绍了C++编程时,大家经常犯得一些内存泄漏方面的编码错误,并给出简单的代码示例。并简要给出了Win32平台下使用检测内存泄漏利器DevPartner BoundsChecker进行检查以发现泄漏代码的详细步骤。值此党的节日,希望对一些迷失在内存泄漏中的同志们有所帮助避免少走弯路。我一直觉得党的党章是完美的,原则是好的,共产主义社会肯定比资本主义财富集中在少数人手里强,只是到了下面执行就有所欠缺了,这次上海闵行封顶房的倒塌正是没有一个良好监督机制的问题,官员参股房地产明显违背政府、法律,希望祖国越来越美好。
作者博客:http://blog.csdn.net/wenhm/
闲话少说,切入正题,本人在参与一个大型Win32软件项目时,对整个项目进行了内存泄漏方面的检查,随着泄漏代码的一个个发现,发现许多的泄漏都具有某些共同性,于是乎总结了一些常见泄漏代码,发给同事们看了。希望能提醒下大家,但后来在项目的二期、三期版本出现的泄漏发现和以前的一些问题几乎是同一性质的,回过来看以前写的一些笔记发现都概括了(当时觉得自己是有那么两把刷子,^_^)。于是想到也许很多编程同志也会碰到同样的问题,能写下来放到网上起到抛砖引玉的作用就更好了。
这里总结下检测出来的内存泄漏有共性的问题,希望对大家以后编程避免内存泄漏有所帮助:
//第一种
1. 在APP开头处包含以下代码
#define CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
2. 在APP初始化的函数中加入
_CrtDumpMemoryLeaks();
3. Output中将会跟踪所有内存创建和销毁的过程,这些信息可以忽略。
4. 程序退出时,output中将会显示出创建内存未释放的代码行信息。
这个可以解决绝大部分情况下出现的内存泄露
//第二种
在所有的CPP文件里加入以下宏定义,
用于在发生内存泄露后,可以定位到进行new操作的代码行数:
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
1. 类内成员动态分配
类所有动态分配的成员变量,一定记得在析构函数中全部进行判断释放内存。当类中有指针成员变量,很多人喜欢在构造函数中来动态分配初始化指针变量但常常忘了在析构函数中来释放内存。当你初来人世,父母疼爱你,当你有了自己的小孩,你又有了生活的重心,此时父母已经被你遗忘了,当你的小孩又有了自己的小孩,你也就被遗忘了。我们很多程序员同志也是这样的,使用时我要用,new得挺爽的,用完不管我事了,这明显是违反我党一贯有始有终的原则的。
示例代码:
class CApple
{
public:
CApple()
{
m_ptrData = new char[128];
}
~CApple()
{
}
}
上面的m_ptrData指向的内存就这样泄漏掉了,记得在析构函数中加上释放的代码,改为如下:
~CApple()
{
if(NULL != m_ptrData)
{
delete m_ptrData;
m_ptrData = NULL;
}
}
需要提醒的是:上面删除m_ptrData再置NULL,是一个良好的编程习惯,可以避免产生野指针。(当然这里对象都析构了不存在这个问题,但其它很多地方将删除的指针置NULL是非常明智的一个做法,不然鬼知道这个指针指向的内存是否是有效的)
2. 指针容器
std::vector<CType>这个错误也是同志们经常犯的,其实很多时候若是简单结构、简单类,你直接用std::vector<CType>就好了,能不用std::vector<CType*>就尽量不用,因为确实很容易忘了vector中原来还存放了要释放的内存的指针,而且在clear或是删除一个元素时都得记起来释放指针指向的内容。
这个很像小时候家里收邮包,邮递员不将邮包送到家里来,也许因为太沉了吧,只是给张包裹单要自己取领。今天忙,往抽屉里一扔,然后就忘了,下次又来一包裹单,又往抽屉里一扔又忘了(指针压入vector),若你不小心将包裹单(指针)弄丢了,你自己都不知道有这么回事(忘了释放内存)。但现在就好多了,快递公司包裹直接送到你手上。
示例代码就不提供了,只能意会不能言传,^_^。
3. 指针赋值
若不是在定义指针代码作用范围内,使用其它地方定义的指针时(比如全局指针,类成员变量指针),进行赋值操作的时候先判断原来指针是否有值,有则先释放原来的内存。
因为若指针原来有值的话,你一覆盖原来分配的内存就再也找不到了,也就产生了泄漏。
代码示例:
void CMainModule::BulidList()
{
m_ptrList = new CList;
….
}
上面的代码,若BuildList跑到第二次时就会出问题了,此时m_ptrList本来就已经指向一块动态分配的内存了,你这时不分青红皂白再new一块赋值过去就将前面动态分配的内存给丢失了。
此时应该先判断m_ptrList是否为NULL,为NULL则new一块内存,否则就应考虑重用原来的内存或是先删除原来再new。
4. 扫尾函数
有些类型对象如CDialog,CWindow,CFile,CImage等需要在Delete前做Close、Release、Destroy等操作的,Delete时检查是否已经调用了相应的扫尾函数。
这个要具体情况具体分析了,比如CDialog的子类销毁时往往需要先调用OnDestroy或是DestroyWindow,不然就可能会存在资源泄漏的问题。
5. 公共模块/第三方库
公共模块一般有init()、open()和release()、terminate()、close()两种类型的函数,不要忘记扫尾类型函数的调用。
在我们这个软件项目中就有用到一个第三方的Av.dll,主要是进行视频编解码方面的库,这个库需要进行初始化才能用,同时也提供了使用完关闭的方法。当时一位同志就忘了调用扫尾函数导致了大量的内存泄漏。这个就要求我们使用第三方库时一定要看仔细使用说明,不要一味冒进。
6. 异常分支
若正常分支有内存需要释放,则不要忘了异常分支的内存释放如try语句的catch分支,函数中的多个return分支都要考虑到相应内存的释放。
示例代码:
try
{
void *ptrData = new char[128];
/// do something …
….
if(NULL != ptrData)
{
delete ptrData;
ptrData = NULL;
}
}
catch(CException &e)
{
LOG(LOG_LEVEL_ERROR, " errorcode:" << e.errorCode());
}
catch(…)
{
LOG(LOG_LEVEL_ERROR, " errorcode:…");
}
上面的代码就没有考虑到两个异常分支也应该要判断指针是否要进行释放的情况。当跑到异常分支中去时就产生了内存泄漏了,这种问题比较难查因为正常情况下程序也是正常不会有泄漏的,能编写代码时就注意就事半功倍了。
7. 动态分配对象数组:
动态分配的对象数组,记得使用delete[]来进行删除。基于两个考虑:
(1)可以释放整个数组的空间;
(2)调用数组中每个对象的析构函数。
第一个其实使用delete加上数组地址一样是可以释放的,因为这块内存是连续分配的,不论采用delete或是delete[]来释放,操作系统都能将这块连续的内存一起释放掉。
但第二点有什么作用呢,此时大家看看 第一章类内成员动态分配 中的示例就知道了,很多释放内存的代码是放在类的析构函数中的,只有使用delete[]才能正确调用析构函数。使用delete是不会调用每个数组元素的析构函数的。
8. 非常规动态内存分配
不是采用常规内存分配(new、malloc、calloc、realloc)的内存也要记得释放,如strdup等。
有一些C/C++ Api返回的指针是动态分配的需要使用者来负责释放,这个只要使用时看清楚Api的说明就不会有什么问题了。
9. 单态模式
最好在程序退出时释放内存,虽然OS会回收,但对于我们以后内存泄漏检测工作能带来极大方便。
虽然单态模式的内存泄漏是一次性泄漏,不会导致内存的不断增加,但因为很多内存泄漏检查工具都是程序正常结束后开始统计内存泄漏的,此时会将单态模式的内存泄漏也统计进去。这样我们就得一个个区分那个是单态泄漏那个是非法泄漏,会带来很大的工作量,若能在程序退出时将单态模式的内存泄漏也释放掉,检测结果就会集中在有问题的内存泄漏上了,大大减少我们的工作量。
解决方法:
为单态模式对象定义DestroyInstance()方法用来释放单态模式的内存,在程序退出时调用该函数。
或是采用static的 smart 指针来让编译器自动在程序退出时负责释放相应的内存。
10. 虚析构函数
一个类的指针被向上引用,作为基类的指针来使用的时候,把析构函数写成虚函数。这样做是为了当用一个基类的指针类型来删除一个派生类的对象时,派生类的析构函数会被调用。(new子类的对象,删除时却采用delete父类类型的指针。new CConcreteClass的对象ptr,但delete CClass类型 的指针ptr,无法调用正确的析构函数)
当针对接口进行编程时,涉及到动态分配的对象指针在各函数间传递时特别要注意将基类的析构函数定义成虚函数。
第一章提到了,若没有正确的调用析构函数,析构函数中若有释放内存的代码就会得不到运行,而且本具体子类中的一些成员变量的析构函数也得不到执行。因为编译器会认为你删除的是一个基类类型的指针,当然就不会去调用子类的成员变量的析构函数的了。
代码示例:
struct ST_Info
{
int iWeight;
char strName[128]
}
class CFruit
{
};
class CApple:public CFruit
{
public:
std::vector< ST_Info> m_vecInfo;
}
CFruit * GetApple()
{
CApple *ptrApple = new CApple();
ST_Info st_Info = {9, “Apple1”};
ptrApple->m_vecInfo.push_back(st_Info);
return ptrApple;
}
void main(int argc, char**argv)
{
CFruit *ptrFruit = GetApple();
delete ptrFruit;
ptrFruit = NULL;
}
上面的代码就会产生内存泄漏了, ptrApple->m_vecInfo中存放的内存将全部泄漏掉,一个能为delete时认为这是一个CFruit *的指针,不会去释放ptrApple->m_vecInfo中元素对应的内存。
修正方法是只要将CFruit的析构函数定义成虚析构函数就OK了。
11. 线程的安全退出,user-interface thread安全退出
和窗口关联的user-interface thread 必须处理WM_DESTROY消息,建议定义一个OnDestroy()函数,该函数调用PostQuitMessage(0)的方法让user-interface thread安全退出,防止线程不安全退出导致内存泄漏。
线程进行安全退出,防止非正常退出的内存泄漏问题。
例子:
LRESULT CMsgReflect::OnDestroy(HWND hWindow, UINT uiMessage, WPARAM uiParam, LPARAM ulParam)
{
PostQuitMessage(0);
return 0;
}
12. 内存动态分配后,在各个分支路径均要考虑是否要释放掉
这个其实和第6章是类似的,下面的代码就没有考虑到执行到continue时的情况会产生内存泄漏。
for (std::vector<TeamInfo>::iterator it = e.teamlist.begin(); it != e.teamlist.end(); it++)
{
FriendGroupData *pGroup=new FriendGroupData;
if(it->unTeamID==DEFAULT_FRIEND_GROUP_ID)
continue;
….
delete pGroup;
}
附录:DevPartner BoundsChecker的使用
1).www .3ddown.com 网上可以下载到8.2版本的DevPartner,进行安装即可。
2).License的下载和安装,http://download.csdn.net/source/828960,运行Distributed License Management,将该license导入即可。
3).将系统时间改成2008年才能使用该license,此时就可以进行内存泄漏的检测了,记得检测完将系统时间改回来就OK了。
4).调整跟踪堆栈的深度,在Visual Studio界面中,DevPartner->Options,然后
Error Detection->Data Collection 即可调整跟踪堆栈的深度了。
参考资料:
【1】.《内存泄漏的检测、定位和解决经验总结》,http://blog.csdn.net/wenhm/archive/2006/06/11/787876.aspx。