摘要:本文简单介绍了C++编程时,大家经常犯得一些内存泄漏方面的编码错误,并给出简单的代码示例。并简要给出了Win32平台下使用检测内存泄漏利器DevPartner BoundsChecker进行检查以发现泄漏代码的详细步骤。 闲话少说,切入正题,本人在参与一个大型Win32软件项目时,对整个项目进行了内存泄漏方面的检查,随着泄漏代码的一个个发现,发现许多的泄漏都具有某些共同性,于是乎总结了一些常见泄漏代码,发给同事们看了。希望能提醒下大家,但后来在项目的二期、三期版本出现的泄漏发现和以前的一些问题几乎是同一性质的,回过来看以前写的一些笔记发现都概括了(当时觉得自己是有那么两把刷子,^_^)。于是想到也许很多编程同志也会碰到同样的问题,能写下来放到网上起到抛砖引玉的作用就更好了。 这里总结下检测出来的内存泄漏有共性的问题,希望对大家以后编程避免内存泄漏有所帮助: 1. 类内成员动态分配 示例代码: 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. 指针容器 这个很像小时候家里收邮包,邮递员不将邮包送到家里来,也许因为太沉了吧,只是给张包裹单要自己取领。今天忙,往抽屉里一扔,然后就忘了,下次又来一包裹单,又往抽屉里一扔又忘了(指针压入vector),若你不小心将包裹单(指针)弄丢了,你自己都不知道有这么回事(忘了释放内存)。但现在就好多了,快递公司包裹直接送到你手上。 示例代码就不提供了,只能意会不能言传,^_^。
3. 指针赋值 因为若指针原来有值的话,你一覆盖原来分配的内存就再也找不到了,也就产生了泄漏。 代码示例: void CMainModule::BulidList() { m_ptrList = new CList; …. } 上面的代码,若BuildList跑到第二次时就会出问题了,此时m_ptrList本来就已经指向一块动态分配的内存了,你这时不分青红皂白再new一块赋值过去就将前面动态分配的内存给丢失了。 此时应该先判断m_ptrList是否为NULL,为NULL则new一块内存,否则就应考虑重用原来的内存或是先删除原来再new。
4. 扫尾函数 这个要具体情况具体分析了,比如CDialog的子类销毁时往往需要先调用OnDestroy或是DestroyWindow,不然就可能会存在资源泄漏的问题。
5. 公共模块/第三方库 在我们这个软件项目中就有用到一个第三方的Av.dll,主要是进行视频编解码方面的库,这个库需要进行初始化才能用,同时也提供了使用完关闭的方法。当时一位同志就忘了调用扫尾函数导致了大量的内存泄漏。这个就要求我们使用第三方库时一定要看仔细使用说明,不要一味冒进。
6. 异常分支 示例代码: 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. 动态分配对象数组: (1)可以释放整个数组的空间; (2)调用数组中每个对象的析构函数。
第一个其实使用delete加上数组地址一样是可以释放的,因为这块内存是连续分配的,不论采用delete或是delete[]来释放,操作系统都能将这块连续的内存一起释放掉。 但第二点有什么作用呢,此时大家看看 第一章类内成员动态分配 中的示例就知道了,很多释放内存的代码是放在类的析构函数中的,只有使用delete[]才能正确调用析构函数。使用delete是不会调用每个数组元素的析构函数的。
8. 非常规动态内存分配 有一些C/C++ Api返回的指针是动态分配的需要使用者来负责释放,这个只要使用时看清楚Api的说明就不会有什么问题了。
9. 单态模式 虽然单态模式的内存泄漏是一次性泄漏,不会导致内存的不断增加,但因为很多内存泄漏检查工具都是程序正常结束后开始统计内存泄漏的,此时会将单态模式的内存泄漏也统计进去。这样我们就得一个个区分那个是单态泄漏那个是非法泄漏,会带来很大的工作量,若能在程序退出时将单态模式的内存泄漏也释放掉,检测结果就会集中在有问题的内存泄漏上了,大大减少我们的工作量。 解决方法: 为单态模式对象定义DestroyInstance()方法用来释放单态模式的内存,在程序退出时调用该函数。 或是采用static的 smart 指针来让编译器自动在程序退出时负责释放相应的内存。
10. 虚析构函数 当针对接口进行编程时,涉及到动态分配的对象指针在各函数间传递时特别要注意将基类的析构函数定义成虚函数。 第一章提到了,若没有正确的调用析构函数,析构函数中若有释放内存的代码就会得不到运行,而且本具体子类中的一些成员变量的析构函数也得不到执行。因为编译器会认为你删除的是一个基类类型的指针,当然就不会去调用子类的成员变量的析构函数的了。 代码示例: 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安全退出 线程进行安全退出,防止非正常退出的内存泄漏问题。 例子: LRESULT CMsgReflect::OnDestroy(HWND hWindow, UINT uiMessage, WPARAM uiParam, LPARAM ulParam) { PostQuitMessage(0); return 0; }
12. 内存动态分配后,在各个分支路径均要考虑是否要释放掉 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的使用 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。
|
C++内存泄漏检查心得
最新推荐文章于 2024-09-13 21:52:35 发布
C++内存泄漏检查心得
2009-07-02 13:07