内存泄漏2——C++中常见内存泄漏情形总结

时间:2021-04-15 20:58:00

出现内存泄露的情形:

1.类成员变量动态分配内存

类所有动态分配的成员变量,一定记得在析构函数中全部进行判断释放内存。当类中动态分配内存的成员一般是指针成员变量。

2.指针容器

使用std::vector<CType*>时,记得在clear或是删除一个元素之前,应该释放指针指向的内容。若是简单结构、简单类,你直接用std::vector<CType>可以避免内存泄漏错误。

3.指针赋值

如果不是定义指针作用范围内,使用其他地方的指针(如全局指针,类成员指针变量)赋值时,首先判断该指针是否为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];
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;
}