说说我对内存泄漏的认识:
说说我对内存泄漏的认识:内存泄漏,对C++程序员来说,一定要非常重视,特别是设计、开发服务器类应用程序的人员。
内存泄漏这个概念我就不深入阐述了,简单的说,无非就是只消耗资源,而不释放,创建了,没有在适当的时候删除。带来的问题就是,程序在运行过程中,占用的内存越来越大,能从任务管理器中发现的,最后因为消耗太多的系统资源,而崩溃。
现在的软件越来越复杂,功能越来越强大,业务逻辑千变万化,这也导致程序难以编写,容易导致错误。对于C++程序员来讲,有两个必备的软件,是对付这类问题的好工具。Boundschecker和purifyplus。前者在调试时候,查找泄漏和错误很容易,后者有linux/unix/windows版本,还能查release版本的泄漏。
Boundschecker使用的时候,一个主要的毛病是,容易误判泄漏,因为在debug的时候,好多代码重载了分配器(包括开发系统自己也这样干),导致最后有大量的报警,很多都是-全局分配器,这个分配器引起的。这就需要仔细分辨了。
这两个工具,都要求你的程序能够正常退出结束,才能取得结果。
查找内存泄漏的时候,一个技巧就是,优先寻找出现次数最多的那条,往往就是错误的根源所在。
是不是所有的内存泄漏,都要消除呢?我认为答案是不确定的,看情况。理论上所有的内存泄漏,都需要消除、修正错误。但实际编程中,的确难以完全做到这点,我们的重点在于,绝对要消灭重复产生的那种类型,如果难以修正或者工程限制,来不及更改,可以放过一次性产生的泄漏问题。
因为在程序运行周期内,只产生一次,等程序结束运行的时候,他使用的所有资源都会被操作系统回收。你就省事了。
当然,这是投机取巧的办法,能消除的,还是尽量消除,因为这往往隐藏着设计漏洞和错误。 我的经验是:
1. 尽量少用new/delete;
2. 如果一定要用new/delete,对应的new和delete的“距离”尽量靠近,并保证从new到delete的路径是安全的,不会由于异常或其它原因离开这条路径;
3. 如果做不到前面两点,就必须使用RAII,用对象来封装new/delete,这是最后的选择;
4. 其它使用new/delete的办法都会很容易导致内存泄漏,与其事后用工具来检查,不如防患于未然。 C++惯用法之RAII(转自水木清华)
C++号称是多范式的通用编程语言, 但是RAII实际上已在C++编程技术中变成不可或缺的核心技术. RAII几乎无处不在的身影不仅仅来自于C++之父的大力提倡, 更来自于这一技术本身的简单, 高效和几乎无所不能的适应面.
如果您还没有听说过RAII的话, 那么我在这里再重新叙述一遍, RAII是下列英文短语的首字母缩写:
Resource Acquisition Is Initialisation
这句话直译为中文的意思是: 资源获得即初始化. 这只是一个短语, 不能指望靠望文生义来了解字面背后的完整含义, 但是短语本身的确反映了重要的论点: 资源是其一, 初始化是其二.
RAII是有关资源的. 资源是一切需要分配的数量有限的资料. 比如, 存储器, 文件句柄, 网络套接字端口, 数据库连接, 以及线程池等. 基本上, 由于物理的限制, 所有的资料都是有限的. 在某些特殊的情况下, 资料由于局部的极大丰富而丧失了资源的意义, 比如沙子, 空气等.但是在大多数情况下, 资料都是有限的, 需要我们善加管理.
资源管理的最基本形式就是善始善终. 申请了资源, 用完了, 就要归还. 在C++程序员生活里最常见的就是内存资源, 资源管理就是内存管理: 申请了内存, 不管什么时候逻辑上完成了对这片内存的使用, 内存就要被正确地释放. 注意这里的用词是"不管什么时候". 在实际应用中, 内存的使用逻辑是如此复杂, 使得逻辑上界定某块内存的生命周期会成为非常繁琐非常复杂的任务, 而内存资源就会在人类智力的疏漏中泄漏出去. 而即使是简单情况, 内存也会在菜鸟程序员手忙脚乱的拙劣中溜走.所以资源管理虽然可以简化为一句"有始有终", 在实际当中很难得到保证.
有一类语言, 比如Java, 把内存资源接管了, 提供了所谓的自动内存管理, 使用内存分配算法的方式为程序员模拟了一个取之不尽用之不竭的准无穷内存模式. 背后的思想是, 在普通应用中, 内存的使用在空间和时间上都是相对集中的, 这就允许用较少的内存来应付时间积累上无限的内存请求. 程序员使用这类语言就不用再考虑内存的释放问题. 负担就大大减轻了.
自动内存管理从原理上把内存资源倍增而产生一种资料(准)无限的虚拟环境, 从而把程序员从繁重的内存资源管理上解放出来, 化更多的精力考虑实际的事务代码, 提高了生产率. 但是它也有自己的局限. 一, 自动内存管理算法比较复杂, 本身的程序就要占一定内存, 同时自动内存管理用时间换空间, 还要求实际物理内存至少为应用最大瞬时所需内存的两倍才能较好地发挥作用, 这一要求说明, 自动内存管理其实已经不是在管理短缺意义上的"资源", 而是为不那么浪费地使用丰富的资料提供一种说得过去的代用方案. 其次, 由于自动内存管理是与具体的应用分离的, 无法知道最合适的切入点, 所以自动内存管理的介入基本是不可预测的. 这限制了自动内存管理在那些对时间响应要求比较严格的程序中的应用. 最后, 自动内存管理仅是对内存资源的管理, 它无法管理其它的资源. 除了内存, 程序员往往要和其它的资源打交道. 自动内存管理模式无法应用到其它类型的资源管理.
C++提供了RAII作为一个真正意义上的资源管理实用方案. 这也是C++语言在资源管理这一意义更加广泛的问题上作出的贡献. 虽然其实用意义如此重大, 但是其做法却很简单, 就是用类来表示资源, 在类的构造函数里分配资源, 在类的析构函数里释放资源. 比如,
class Resource {
public:
Resource(const char *name) : _resource(alloc_resource(name)) {...}
~Resource() { release_resource(_resource); }
};
资源类的使用也很简单, 按泛围使用. 比如, 有一个事务处理, 使用到了某种资源. 如果这一事务可以用一个函数来表示, 那么, 可以简单地用一个在函数入口处分配的资源变量来表示资源分配. 例如:
void transaction1(const char *res_name)
{
Resource res(res_name);
// 后面是使用资源res
}
不管程序体内资源res的使用逻辑如何复杂, 退出路径如何繁多, C++语言保证了在退出函数范围的时候, 资源对象必定得到析构, 资源必定得到释放. 这一保证甚至包括底层函数抛出异常的情况. 所有这些都是免费的, 程序员要做的, 就是用一个RAII语义的类来表达一类资源, 然后用一对标识代码范围的大括号来构勒资源的每一个生命周期范围. 如果该事务逻辑过于复杂, 无法有效地在单一函数里表达, 那么可以用一个类来表达该事务, 这个类可以简单地把用到的RAII资源作为成员包含, 在表达逻辑上达到了资源和事务逻辑共生死的地步.
你会说, 这太不够用了, 资源的生命周期可能是动态的, 无法静态决定. 有时候甚至是外部用户决定的. 遇上这种情况, 智能指针类(其本身就有RAII语义)的引用计数基本上可以解决百分之九十以上的问题.
例如在一个很实际的应用中, 一个事务可能由用户通过界面发起执行, 发起后可能由于外部资源失败而中止, 可能由于用户命令而中止, 也有可能是自然执行完毕而中止.
一个比较自然的表达是线程池和事务函数. 接到用户命令, 主程序选择可用的闲置线程, 载入该事务函数运行, 一旦事务函数因为故障或者自然原因返回, 线程重新回到闲置状态. 事务函数和主程序用数据同步通讯的方式来实现事务运行状态的控制. 在这种方式中, 资源成为函数的一部分, 其生死已基本不是我们关心的问题, 我们只要关心, 何时调用事务函数. 这已经是比较大比较接近事务逻辑的问题了.
如果不能使用线程, 或者认为多线程管理要比资源管理还要微妙还要邪恶, 那么就要写所谓的异步过程. 整个的逻辑就是要在一个线程里通过轮询和动作切割的方式来实现事务和事件的多道并行处理, 这仍然可以通过写一个异步事务类来表达, 资源将作为成员附着在该事务类上, 而所有的异步事务类将作为资源被轮询循环所在的函数自动管理(这是必然的, 主循环需要轮流执行当前正在执行的事务).
可见, 通过RAII程序员成功地把资源管理问题弱化, 转为如何表达事务逻辑上. 而这正是程序员的主要任务. 也就是说, 一旦资源被用RAII的形式封装起来, 程序员就不再考虑资源泄漏问题, 而考虑如何表达事务逻辑的问题, 这个代价并不算大. 当然, 程序员要坚持只用资源类的对象形式而不是显式动态分配的形式(也就是函数里的普通变量或者事务类里的普通成员, 而不是任何new出来的对象形式), 否则所有的努力都白费了.这算是一点点代码要求. 并不难做到.
和自动内存管理比较起来, RAII仅需少量的管理代码(对类不对对象), 能普遍适用于各种资源对象的使用, 时间上可以控制和预测. 能为资源管理提供一个统一的模式. RAII是自由的, 它更多是靠程序员对规范的简单遵守(坚持使用对象而不是指针)来达到目的. 我认为, 程序员是需要遵守纪律的, 特别是那些好的纪律.
使用RAII应该成为C++程序员的基本习惯, 这正如书写无错高效代码应该成为每个C++程序员的追求. 运行时内存泄漏检测(堆内存不变化保证)
BoundChecker和purifyplus都需要在程序结束后才能发现内存问题,而对于长期运行的服务程序,这种要求有点过分.
我们有一个服务程序,由十几个组件组成,突然发现了严重地内存使用持续增加,为了找到内存泄漏的地方,使用了下面的程序.
在我们明确堆内存应该不变的地方使用HEAP_UNCHANGE;如在new出一块内存之前,如果之后没有delete该内存,就会有检测信息.这样,在程序调试时就知道内存有无问题.
下面的程序(VC6.0下开发)能够在运行进行内存泄漏检测,其基本原理是在HEAP_UNCHANGE时,记录下堆内存使用情况,在退出作用范围时比较堆内存使用的变化.如果有变化,就报告有内存泄漏.
它还可检查出COM组件间调用时的内存问题,通过COM内存分配器进行的内存分配问题都能检查出来.
但是,对于象vector,list或其它自己重载了内存管理的类,无法准确地检查出内存问题.
通过使用这个方法,我们发现了ostrstream在进行str()之后,如果没有调用freeze(false),ostrstream的析构函数不会释放字节串的内存.尽管后来查msdn,上面也说明了这一点.
map类使用了额外的内存,只有在程序完全退出才释放.可以将HEAP_UNCHANGE放在main(int argc, char* argv[])的第一行上进行验证.
mobj.h
class MOBJ_API McHeapCheck
{
_CrtMemState s1_;
char* filename_;
long lineno_;
public:
McHeapCheck(const char* filename, const long lineno);
~McHeapCheck();
};
MOBJ_API void RegisterMallocSpy(bool bStart);
//McHeapCheck类,它使用了C运行库的内存检测函数,来检查是否有内存泄漏。
//为了能在组件间检查出内存泄漏,
//在CoInitialize(NULL);之后调用RegisterMallocSpy(true);
//在::CoUninitialize();之前调用RegisterMallocSpy(false);
//内存检测完成后,可以将上述添加的语句注掉
//McHeapCheck类采用了ScopeGuard模式,在退出作用域后自动进行堆内存的检查。
//因此,在使用这个类时,要确保在期望没有堆内存未释放的作用域上使用。
#ifdef _DEBUG
#define HEAP_UNCHANGE McHeapCheck __heapCheck(__FILE__, __LINE__)
#else
#define HEAP_UNCHANGE
#endif
mobj.cpp
const char szNull[] = {"NULL"};
const ULONG HEADERSIZE = 4;
const char szInCom[] = {"inCOM"};
McHeapCheck::McHeapCheck(const char* filename, const long lineno)
{
lineno_ = lineno;
filename_ = (char*)malloc(filename ? (strlen(filename)+1) : sizeof(szNull));
_CrtMemCheckpoint(&s1_);
strcpy(filename_ , filename ? filename : szNull);
}
McHeapCheck::~McHeapCheck()
{
_CrtMemState s2, s3;
_CrtMemCheckpoint(&s2);
if(_CrtMemDifference(&s3, &s1_, &s2))
{
_RPT2(_CRT_WARN, "%s(%d): >>>>>>>>>>heap memory leak begin:>>>>>>>>>>\n", filename_, lineno_);
_CrtMemDumpAllObjectsSince(&s1_);
_RPT2(_CRT_WARN, "%s(%d): <<<<<<<<<<heap memory leak end:<<<<<<<<<<<<\n", filename_, lineno_);
}
free(filename_);
}
class MOBJ_API McMallocSpy : public IMallocSpy
{
public:
McMallocSpy(void);
~McMallocSpy(void);
// IUnknown methods
STDMETHOD(QueryInterface) (REFIID riid, LPVOID *ppUnk);
STDMETHOD_(ULONG, AddRef) (void);
STDMETHOD_(ULONG, Release) (void);
// IMallocSpy methods
STDMETHOD_(ULONG, PreAlloc) (ULONG cbRequest);
STDMETHOD_(void *, PostAlloc) (void *pActual);
STDMETHOD_(void *, PreFree) (void *pRequest, BOOL fSpyed);
STDMETHOD_(void, PostFree) (BOOL fSpyed);
STDMETHOD_(ULONG, PreRealloc) (void *pRequest, ULONG cbRequest,
void **ppNewRequest, BOOL fSpyed);
STDMETHOD_(void *, PostRealloc) (void *pActual, BOOL fSpyed);
STDMETHOD_(void *, PreGetSize) (void *pRequest, BOOL fSpyed);
STDMETHOD_(ULONG, PostGetSize) (ULONG cbActual, BOOL fSpyed);
STDMETHOD_(void *, PreDidAlloc) (void *pRequest, BOOL fSpyed);
STDMETHOD_(BOOL, PostDidAlloc) (void *pRequest, BOOL fSpyed, BOOL fActual);
STDMETHOD_(void, PreHeapMinimize) (void);
STDMETHOD_(void, PostHeapMinimize) (void);
private:
ULONG m_cRef;
ULONG m_cbRequest;
};
McMallocSpy::McMallocSpy(void)
:m_cRef(0)
{
}
McMallocSpy::~McMallocSpy(void)
{
}
HRESULT McMallocSpy:ueryInterface(REFIID riid, LPVOID *ppUnk)
{
HRESULT hr = S_OK;
if (IsEqualIID(riid, IID_IUnknown))
*ppUnk = (IUnknown *) this;
else if (IsEqualIID(riid, IID_IMallocSpy))
*ppUnk =(IMalloc *) this;
else
{
*ppUnk = NULL;
hr =E_NOINTERFACE;
}
AddRef();
return hr;
}
ULONG McMallocSpy::AddRef(void)
{
return ++m_cRef;
}
ULONG McMallocSpy::Release(void)
{
if (--m_cRef == 0)
delete this;
return m_cRef;
}
ULONG McMallocSpy:reAlloc(ULONG cbRequest)
{
m_cbRequest = cbRequest;
return cbRequest + HEADERSIZE;
}
void *McMallocSpy:ostAlloc(void *pActual)
{
//HEADER的前4字节是通过malloc申请的内存地址
char* pAlloc = (char*)malloc(sizeof(szInCom) + sizeof(long));
strcpy(pAlloc, szInCom);
char *p = pAlloc + sizeof(szInCom);
*(long*)p = m_cbRequest;
*(char**)pActual = pAlloc;
return (void *) (((BYTE *) pActual) + HEADERSIZE);
}
void *McMallocSpy:reFree(void *pRequest, BOOL fSpyed)
{
if (pRequest == NULL)
return NULL;
if (fSpyed)
{
void* pActual = (void *) (((BYTE *) pRequest) - HEADERSIZE);
//释放申请的内存地址
char* pAlloc = *(char**)pActual;
free(pAlloc);
return pActual;
}
else
{
return pRequest;
}
}
void McMallocSpy:ostFree(BOOL fSpyed)
{
return;
}
ULONG McMallocSpy:reRealloc(void *pRequest, ULONG cbRequest,
void **ppNewRequest, BOOL fSpyed)
{
if (fSpyed)
{
*ppNewRequest = (void *) (((BYTE *) pRequest) - HEADERSIZE);
return cbRequest + HEADERSIZE;
}
else
{
*ppNewRequest = pRequest;
return cbRequest;
}
}
void *McMallocSpy:ostRealloc(void *pActual, BOOL fSpyed)
{
if (fSpyed)
return (void *) (((BYTE *) pActual) + HEADERSIZE);
else
return pActual;
}
void *McMallocSpy:reGetSize(void *pRequest, BOOL fSpyed)
{
if (fSpyed)
return (void *) (((BYTE *) pRequest) - HEADERSIZE);
else
return pRequest;
}
ULONG McMallocSpy:ostGetSize(ULONG cbActual, BOOL fSpyed)
{
if (fSpyed)
return cbActual - HEADERSIZE;
else
return cbActual;
}
void *McMallocSpy:reDidAlloc(void *pRequest, BOOL fSpyed)
{
if (fSpyed)
return (void *) (((BYTE *) pRequest) - HEADERSIZE);
else
return pRequest;
}
BOOL McMallocSpy:ostDidAlloc(void *pRequest, BOOL fSpyed, BOOL fActual)
{
return fActual;
}
void McMallocSpy:reHeapMinimize(void)
{
return;
}
void McMallocSpy:ostHeapMinimize(void)
{
return;
}
MOBJ_API void RegisterMallocSpy(bool bStart)
{
bStart ? CoRegisterMallocSpy(new McMallocSpy) : CoRevokeMallocSpy();
} memory leak is not big headache. memory corruption is really nightmare.
I'm wondering any of your tools can detect code like below:
char * p1=new char;
delete []p1;
char * p2=new char;
strcpy(p2, "56789");
strcpy(p1,"12345"); //corruption. purify应该能够查到,尽管程序会崩溃。 和自动内存管理比较起来, RAII仅需少量的管理代码(对类不对对象), 能普遍适用于各种资源对象的使用, 时间上可以控制和预测. 能为资源管理提供一个统一的模式. RAII是自由的, 它更多是靠程序员对规范的简单遵守(坚持使用对象而不是指针)来达到目的. 我认为, 程序员是需要遵守纪律的, 特别是那些好的纪律.
坚持使用对象而不是指针?如何理解? Did you try it? I can tell you it won't crash on most platforms.
you can bet on beers with it. (:DD
If purify can detect it , I will be really impressed. 其实大多数情况下,我们无须编写过于复杂/优化的代码。
封装的特性,就是把数据和方法帮定在一起。
如果必须在各个类之间交换内存的话,不妨单独做一个类似smart指针的类也很好啊。
个人以为,最好的代码应该是便于迅速阅读理解以及调试的代码。 使用带引用计数的智能指针,这种指针实现的代码很多。
页:
[1]