之所以撰写这篇文章是因为前段时间花费了很大的精力在已经成熟的代码上再去处理memory leak问题。写此的目的是希望我们应该养成良好的编码习惯,尽可能的避免这样的问题,因为当你对着一大片的代码再去处理此类的问题,此时无疑增加了解决的成本和难度。准确的说属于补救措施了。
1. 什么是内存泄漏(memory leak)?
指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
A memory leak is a particular type of unintentional memory consumption by a computer program where the program fails to release memory when no longer needed. This condition is normally the result of a bug in a program that prevents it from freeing up memory that it no longer needs.This term has the potential to be confusing, since memory is not physically lost from the computer. Rather, memory is allocated to a program, and that program subsequently loses the ability to access it due to program logic flaws.
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 :对于系统资源使用之前要仔细看起使用方法,防止错误使用或者忘记释放掉系统资源。
我们看MSDN上一个创建字体的例子:
示例代码
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++复杂老练的对象管理设施来管理资源.(有待完善)
例2:
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。这也是大家编码过程中常常使用的方法。
下面给出简单的sample来说明。
示例代码
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使用crtdbg来检测到内存泄露很简单,只要在文件的第一行定义_CRTDBG_MAP_ALLOC,然后include头文件crtdbg.h,在程序需要内存检测的地方调用_CrtDumpMemoryLeaks,就可以输出内存泄露的信息,如上面的程序,我们申请了100个字节的内存而没有释放,但是我们可以很清楚地看到内存泄露在 哪个地方。
< stdlib.h >
#include
< crtdbg.h >
int main( void )
{
char * p = ( char * )malloc( sizeof ( char ) * 100 );
_CrtDumpMemoryLeaks();
}
我们在main.cpp这个文件中的第八行申请了内存,但是没有进行释放
那么编译器是怎么知道我们有内存泄露呢??就是利用宏定义把我们的调用的malloc替换成crtdbg 库里面的_malloc_dbg,我们在申请内存的时候,_malloc_dbg会先记录下我们申请内存的行数以及大小(记得编译器有内置的宏定义__LINE__和__FILE__不?),把这些信息放到一个list(只是举例,使用list保存这些信息一旦程序大了会很慢)里面,当我们free内存的时候,把这块内存的信息从list里面删除掉,我们调用_CrtDumpMemoryLeaks()的时候就是在把这个list的信息依次打印出来而已
下面是我们定义_CRTDBG_MAP_ALLOC后实际上所调用的malloc原型,malloc已经成了一个宏定义
#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__)
当然,我们一般调用_CrtDumpMemoryLeaks的时候都是在程序的结尾处,如果我们的程序有多个出口,我们值需要在程序开始处调用_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ) 就可以了
有时候我们需要检测某一段代码是否有内存泄露,crtdbg库也可以帮我们做到
_CrtMemState s1;
_CrtMemState s2;
_CrtMemCheckpoint( & s1);
char * p2 = ( char * )malloc( 400 );
_CrtMemCheckpoint( & s2);
_CrtMemState s3;
if (_CrtMemDifference( & s3, & s1, & s2))
{
_CrtMemDumpStatistics( & s3);
}
这样,我们在输出窗口将会看到s1和s2之间的内存使用信息:
0 bytes in 0 Free Blocks. 400 bytes in 1 Normal Blocks. 0 bytes in 0 CRT Blocks. 0 bytes in 0 Ignore Blocks. 0 bytes in 0 Client Blocks. Largest number used: 0 bytes. Total allocations: 400 bytes.
crtdbg库也有缺点,当你使用一个别人提供的lib或者dll库的时候,你调用这个函数,这个函数分配了内存,需要你去调用另外一个函数才能把内存释放掉,但是你不知道这个函数需要调用另外一个函数才能释放掉内存,这个是无法通过crtdbg库检测出来的,这个函数包括c++的new函数,所以这个库实际上不适用C++
利用share_ptr来管理内存
如果有使用过boost库的应该知道,boost里面有一个shart_ptr被誉为神器,因为他可以帮我们自动管理内存,具体用法很简单:
boost::shared_ptr < connection > p ( new connection());
这样的话我们不需要去delete内存,shartd_ptr会在我们不需要这快内存的时候帮我们delete掉,shartd_ptr内部是使用引用计数以及C++的RAII,有别的对象引用该指针的时候引用技术就+1,shartd_ptr析构函数调用的时候引用计数就-1,当为0的时候就delete掉该指针,所以我们并不需要调用delete来释放资源,share_ptr会帮我们管理
shared_ptr虽然看起来很好用,但是当程序一旦复杂起来,shared_ptr依然也会变复杂(shared_ptr四宗罪),当然boost本身就比较复杂,这个也是我比较不喜欢boost的一个原因
将资源集中管理
这个也是我比较经常使用的方法,特别是在大程序的使用,配合单件模式,将资源在整个程序或者模块中集中管理,这样在程序结束的时候只要我们在析构函数里面有清理这些资源,我们就可以避免内存泄露,对于数据的一些写操作全部在这个类中统一操作,如果要暴露内部的数据,只对外提供const数据(可以通过强转去掉const属性)
当然这个方法并不适用于所有场景,比如我们需要提供库给别人,我们没办法预测到客户需要什么操作,所以这个方法只适用内部团队开发
总之内存管理据我所知到现在还是没有什么好的解决方法,特别是当代码一旦膨胀的时候,到现在好像java,python,erlang都有内存泄露的问题,我们只能在平常开发中多注意了
参考资料:
本文相关链接:http://blog.csdn.net/na_he/article/details/7429171
陈硕的博客(有一些shared_ptr的资料,也可以从这里看出shared_ptr使用起来没那么简单)
shared_ptr四宗罪
MSDN crtdbg库
http://www.cnblogs.com/linyilong3/archive/2013/03/23/2977247.html