之所以撰写这篇文章是因为前段时间花费了很大的精力在已经成熟的代码上再去处理memory leak问题。写此的目的是希望我们应该养成良好的编码习惯,尽可能的避免这样的问题,因为当你对着一大片的代码再去处理此类的问题,此时无疑增加了解决的成本和难度。准确的说属于补救措施了。
1. 什么是内存泄漏(memory leak)?
指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
2. 对于C和C++这种没有Garbage Collection 的语言来讲,我们主要关注两种类型的内存泄漏:
堆内存泄漏(Heap leak)。堆内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
系统资源泄露(Resource Leak).主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
3. 如何解决内存泄露?
内存泄露的问题其困难在于1.编译器不能发现这些问题。2.运行时才能捕获到这些错误,这些错误没有明显的症状,时隐时现。3.对于手机等终端开发用户来说,尤为困难。下面从三个方面来解决内存泄漏:
第一,良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄漏。当程式稳定之后,再来检测内存泄漏时,无疑增加了排除的困难和复杂度。使用了内存分配的函数,要记得要是用其想用的函数释放掉
Heap memory
malloc\realloc ------free
new\new[] ------delete\delete[]
GlobalAlloc ------GlobalFree
要特别注意数组对象的内存泄漏
MyPointEX *pointArray =new MyPointEX [100];
其删除形式为:
delete []pointArray
Resource Leak:对于系统资源使用之前要仔细看使用方法,防止错误使用或忘记释放掉系统资源。
RECT rect;
HBRUSH hBrush;
FONT hFont;
hdc = BeginPaint(hWnd, &ps);
hFont = reateFont(48,0,0,0,FW_DONTCARE,FALSE,TRUE,FALSE,DEFAULT_CHARSET,OUT_OUTLINE_PRECIS, CLIP_DEFAULT_PRECIS,CLEARTYPE_QUALITY, VARIABLE_PITCH,TEXT("Impact"));
SelectObject(hdc, hFont);
SetRect(&rect, 100,100,700,200);
SetTextColor(hdc, RGB(255,0,0));
DrawText(hdc, TEXT("Drawing Text with Impact"), -1,&rect, DT_NOCLIP);
DeleteObject(hFont);
EndPaint(hWnd, &
如果使用完成的时候忘记释放字体,就造成了资源泄露。
对于基于引用计数的系统对象尤其要注意,因为只有其引用计数为0时,该对象才能正确被删除。而其使用过程中有其生成的新的系统资源,使用完毕后,如果没有及时删除,都会影响其引用计数。
IDNS *m_pDns//define a DNS object.
If(NULL == m_pDns)
{
IEnv_CreateInstance (m_pEnv,AEECLSID_DNS,(void **) (&m_pDns))
}
If(m_pDns)
{
Char szbuff[256];
IDNS_AddQuestions(M_pDns,AEEDNSTYPE_A,ADDDNSCLASS_IN,szbuff);
IDNS_Start(m_pDns,this);
const AEEDNSResponse * pDnsResponse = NULL;
IDNS_GetResponse(pMe->m_pDns, &pDnsResponse);
…………………………………………………………
…………………………………………………………..
………………………………………………………..
DNS_Release(pMe->m_pDns);//当程序运行到此时,其返回值不是0,是1,其含义是程序已经产生内存泄露了,系统已经有一个由DNS所产生的内核对象没有释放,而当这段代码多次执行之后,内存泄露将不断增加……..
m_pDns=NULL;
}
看起来很不直观,仔细分析就会发现,对象pDnsResponse是从m_pDns产生新的object,所以m_pDns的引用计数会增加,因此在使用完pDnsResponse,应该release 该对象使其引用计数恢复正常。
对于资源,也可使用
RAII,RAII(Resource acquisition is initialization)
资源获取即初始化,它是一项很简单的技术,利用C++对象生命周期的概念来控制程序的资源,例如内存,文件句柄,网络连接以及审计追踪(audit trail).
RAII的基本技术原理很简单.若希望保持对某个重要资源的跟踪,那么创建一个对象,并将资源的生命周期和对象的生命周期相关联.如此一来,就可以利用C++复杂老练的对象管理设施来管理资源.
Struct ITypeface *pTypeface;
if (pTypeface)
{
IANY_CreateInstance(g_pApplet->m_pIShell,AEECLSID_BTFETypeface,void**)& Typeface);
}
接下来我们就可以从这个接口上面创建字体,比如
IHFont **pihf=NULL;
ITypeface_NewFontFromFile(ITypeface,……,&pihf).
ITypeface_NewFontFrommemory(ITypeface,……..,&pihf)
ITypeface_NewFontFromClassID(IType,……,&pihf)
但是要切记,这些字体在使用完成后一定要release掉,否则最后iTypeface的引用计数就是你最后没有删除掉的字体的个数。
第二,重载 new 和 delete。这也是大家编码过程中常常使用的方法。
memchecker.h
structMemIns
{
void * pMem;
int m_nSize;
char m_szFileName[256];
int m_nLine;
MemIns * pNext;
};
classMemManager
{
public:
MemManager();
~MemManager();
private:
MemIns *m_pMemInsHead;
int m_nTotal;
public:
static MemManager* GetInstance();
void Append(MemIns *pMemIns);
void Remove(void *ptr);
void Dump();
};
void *operatornew(size_tsize,constchar*szFile, int nLine);
void operatordelete(void*ptr,constchar*szFile, int nLine);
void operatordelete(void*ptr);
void*operatornew[] (size_tsize,constchar*szFile,int nLine);
void operatordelete[](void*ptr,constchar*szFile, int nLine);
void operatordelete[](void *ptr);
memechecker.cpp
#include"Memchecher.h"
#include<stdio.h>
#include<malloc.h>
#include<string.h>
MemManager::MemManager()
{
m_pMemInsHead=NULL;
m_nTotal=NULL;
}
MemManager::~MemManager()
{
}
voidMemManager::Append(MemIns *pMemIns)
{
pMemIns->pNext=m_pMemInsHead;
m_pMemInsHead = pMemIns;
m_nTotal+= m_pMemInsHead->m_nSize;
}
voidMemManager::Remove(void *ptr)
{
MemIns * pCur = m_pMemInsHead;
MemIns * pPrev = NULL;
while(pCur)
{
if(pCur->pMem ==ptr)
{
if(pPrev)
{
pPrev->pNext =pCur->pNext;
}
else
{
m_pMemInsHead =pCur->pNext;
}
m_nTotal-=pCur->m_nSize;
free(pCur);
break;
}
pPrev = pCur;
pCur = pCur->pNext;
}
}
voidMemManager::Dump()
{
MemIns * pp = m_pMemInsHead;
while(pp)
{
printf( "File is %s\n", pp->m_szFileName );
printf( "Size is %d\n", pp->m_nSize );
printf( "Line is %d\n", pp->m_nLine );
pp = pp->pNext;
}
}
voidPutEntry(void *ptr,intsize,constchar*szFile, int nLine)
{
MemIns * p = (MemIns *)(malloc(sizeof(MemIns)));
if(p)
{
strcpy(p->m_szFileName,szFile);
p->m_nLine = nLine;
p->pMem = ptr;
p->m_nSize = size;
MemManager::GetInstance()->Append(p);
}
}
voidRemoveEntry(void *ptr)
{
MemManager::GetInstance()->Remove(ptr);
}
void *operatornew(size_tsize,constchar*szFile, int nLine)
{
void * ptr = malloc(size);
PutEntry(ptr,size,szFile,nLine);
return ptr;
}
voidoperatordelete(void *ptr)
{
RemoveEntry(ptr);
free(ptr);
}
void operatordelete(void*ptr,constchar * file, intline)
{
RemoveEntry(ptr);
free(ptr);
}
void*operatornew[] (size_tsize,constchar* szFile,intnLine)
{
void * ptr = malloc(size);
PutEntry(ptr,size,szFile,nLine);
return ptr;
}
void operatordelete[](void *ptr)
{
RemoveEntry(ptr);
free(ptr);
}
void operatordelete[](void*ptr,constchar*szFile,intnLine)
{
RemoveEntry(ptr);
free(ptr);
}
#definenewnew(__FILE__,__LINE__)
MemManagerm_memTracer;
MemManager*MemManager::GetInstance()
{
return &m_memTracer;
}
void main()
{
int *plen =newint ;
*plen=10;
delete plen;
char *pstr=newchar[35];
strcpy(pstr,"hello memory leak");
m_memTracer.Dump();
return ;
其主要思路是将分配的内存以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表,其中记录了内存泄露的文件,所在文件的行数以及泄露的大小哦。
第三,Boost 中的smart pointer(待完善,结合大家的建议)
第四,一些常见的工具插件,详见我的Blog中相关文章。
4. 由内存泄露引出内存溢出话题:
所谓内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足要取,于是会产生内存溢出的问题。
常见的溢出有:内存分配未成功,却使用了它。常用的解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理.
内存分配虽然成功,但是尚未初始化就引用他。 内存分配成功并且已经初始化,但操作越过了内存的边界。 例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for 循环语句中,循环次数很容易搞错,导致数组操作越界。
使用free或delete释放了内存后,没有将指针设为NULL。导致产生“野指针”。
不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
windows下如何防止内存泄漏:
在windows下开发C++程序的时候,我们经常需要用到malloc申请内存,然后利用free回收内存,但是开发人员的不小心可能会忘记free掉内存,这样就导致了内存泄露。
利用库检测内存泄露消息
#define _CRTDBG_MAP_ALLOC //如果没有这个宏定义,我们只能知道有内存泄露,却无法知道在哪个地方申请内存忘记了释放
#include
<stdlib.h>
#include
<crtdbg.h>
int main(void)
{
char *p = (char *)malloc(sizeof(char) * 100);
_CrtDumpMemoryLeaks();
}
使用crtdbg来检测到内存泄漏很简单,只要在文件的第一行定义
_CRTDBG_MAP_ALLOC,然后include头文件crtdbg.h,在程序需要内存检测的地方调用_CrtDumpMemoryLeaks,就可以输出内存泄漏的信息。
我们在main.cpp这个文件中的第8行申请了内存,但是没有进行释放。
那么编译器怎么知道我们有内存泄漏呢??就是利用宏定义把我们调用的malloc替换成crtdbg库里面的_malloc_dbg会先记录下我们申请内存的行数以及大小(记得编译器有内置的宏定义__LINE__和__FILE__不?)把这些信息放到一个list(只是举例,使用list保存这些信息,一旦程序大了会很慢)里面,当我们free内存的时候,把这块内存的信息从list里面删除掉,我们调用_CrtDumpMemoryLeaks()就是把这个list信息依次打印出来而已。
当然,我们一般调用_CrtDumpMemoryLeaks的时候都是在程序结尾处,如果我们的程序有多个出口,我们只需要在程序开始处调用_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)就可以。
_CrtMemState s1;
_CrtMemState s2;
_CrtMemCheckpoint(&s1);
char *p2 = (char *)malloc(400);
_CrtMemCheckpoint(&s2);
_CrtMemState s3;
if (_CrtMemDifference(&s3,&s1,&s2))
{
_CrtMemDumpStatistics(&s3);
}
crtdbg库也有缺点,当你使用别人使用的lib或dll库的时候,你调用这个函数,这个函数分配了内存,需要你去调用另外一个函数才能把内存释放掉,但是你不知道这个函数需要调用另外一个函数才能释放掉内存,这个是无法通过crtdbg库检测出来的,这个函数包括C++的new函数,所以这个库实际上不适用C++
利用share_ptr来管理内存
如果有使用过boost库的应该知道,boost里面有一个share_ptr被誉为神器。因为他可以帮我们自动管理内存,具体用法很简单:
boost::shared_ptr < connection > p ( new connection());
这样的话我们不需要去delete内存,share_ptr会在我们不需要内存的时候帮我们delete掉,shartd_ptr内部是使用引用计数以及C++的RAII,有别的对象引用该指针的时候引用计数就+1,shartd_ptr析构函数调用的时候引用计数就减一,当为0的时候就delete掉该指针,所以我们并不需要delete来释放资源,share_ptr会帮我们整理。
将资源集中管理
这个也是我比较经常使用的方法,特别是在大程序的使用,配合单件模式,将资源在整个程序或模块中集中管理,这样在程序结束的时候只要 我们在析构函数里面有清理这些资源,我们就可以避免内存泄露,对于数据的一些写操作全部在这个类中统一操作,如果要暴露内部的数据,只对外提供const数据