内存管理和堆调试
luyued 发布于 2011-01-13 02:01 浏览 N 次在Debug版下,运行时库跟踪内存的分配和释放。运行时库使用下面的结构来完成一些簿记工作:
typedef struct _CrtMemBlockHeader
{
// Pointer to the block allocated just before this one:
struct _CrtMemBlockHeader *pBlockHeaderNext;
// Pointer to the block allocated just after this one:
struct _CrtMemBlockHeader *pBlockHeaderPrev;
char *szFileName; // File name
int nLine; // Line number
size_t nDataSize; // Size of user block
int nBlockUse; // Type of block
long lRequest; // Allocation number
// Buffer just before (lower than) the user's memory:
unsigned char gap[nNoMansLandSize];
} _CrtMemBlockHeader;
/* In an actual memory block in the debug heap,
* this structure is followed by:
* unsigned char data[nDataSize];
* unsigned char anotherGap[nNoMansLandSize];
*/
为了精确地控制内存的分配和跟踪,运行时库和MFC提供了多个方案和可选参数。注意上面结构中的nBlockUse变量,它可以取值为:
_NORMAL_BLOCK 对运行时库函数Malloc和Calloc的调用导致分配类型为_NORMAL_BLOCK的内存块。
_CRT_BLOCK 表明这个内存块是由运行时库分配和管理的。一般来说,你无需考虑这些内存块是否会造成泄漏或损坏。
_CRT_CLIENT_BLOCK 可以将需要特殊跟踪的内存块置为这个标志。比如在MFC中,将所有的CObject堆对象置为这个类型。分配这种类型的内存块需要直接调用堆调试函数,同时,运行时库允许你在这个类型下自定义类型子集,具体参考MSDN中的Debuggin\Solving Buffer Overwrites and Memory Leaks\Types of Blocks on the Debug Heap主题。
_FREE_BLOCK 通常内存一旦释放就归还给操作系统,不再受运行时库管理。因此运行时库提供这个标志指示释放的内存延迟归还,这样这些内存就不会再次被分配。这样做有几个用处,其一是可以模拟内存不足的情况,另一个用处是帮助检查程序中是否有引用已释放的内存的情况。
_IGNORE_BLOCK 也可以在某段时间内关闭对内存分配的监控,这时分配的内存块都被标记为这种类型。在此后打开某些监控时,也不检查这些内存块。
运行时库提供了相应的操作函数来分配、释放内存,并监控和管理这些状态标志。
在Debug版下,运行时库使用malloc_debug来分配内存。这个函数原型是:
void *_malloc_dbg( size_t size, int blockType, const char *filename, int linenumber );
可以看这个函数完全填充了前面的内存分配结构中的待定成员变量,这里的blockType的取值仅限于上面列出的_NORMAL_BLOCK和_CRT_CLIENT_BLOCK两种。与之对应,_free_dbg释放内存,它只要两个参数就够了。
还有其它一些函数用于分配和释放内存,这里从略。
运行时库还提供了全局变量_crtDbg_Flag来指示内存分配、释放和检查时的行为。它的取值是一系列标志位的或:
_CRTDBG_ALLOC_MEM_DF 这个标志位指示内存分配函数在分配内存时将新分配的内存块加入监视。将_CrtMemBlockHeader的nBlockUse成员变量为_IGNORE_BLOCK即可关掉这种监视,为其它值是为打开。
_CRTDBG_DELAY_FREE_MEM_DF 这个标志位指示内存释放函数在释放内存时,将_CrtMemBlockHeader的nBlockUse成员变量置为_FREE_BLOCK,且不将其从运行时库内存堆中移出。
_CRTDBG_CHECK_ALWAYS_DF 这个标志位指示运行时库在每次释放内存时都对被操作的内存块进行检查。这种检查可以查出内存访问越界和引用无效内存的情况。
_CRTDBG_CHECK_CRT_DF 这个标志正如它的字面意义所说的那样,还对标志为_CRT_BLOCK的内存块进行检查。
_CRTDBG_LEAK_CHECK_DF 这个标志指示运行时库在退出时对内存泄漏的情况进行检查。
函数_crtSetDbgFlag()用来读取和修改全局变量_crtDbg_Flag。尽管程序员能够直接访问到全局变量,运行时库还是决定使用一个函数来访问它,以便程序代码在无须改动的情况下,就可以在Debug和Release版两个编译模式下工作。在Release版下,所有对_crtSetDbgFlag()的调用都在预处理时被注释掉。
现在内存的监视和跟踪就很容易了。在Debug版模式下分配内存时,实际分配的是包含了data[nDataSize]和anotherGap[nNoMansLandSize]成员的一个数据结构,但返回值是data[nDataSize]的首地址。内存分配函数在返回以前,首先将新分配的内存区逐字节初始化为0xcd,前后两个隔离带(gap)逐字节初始化为0xfd;并将其类型标记为_NORMAL_BLOCK、_CRT_CLIENT_BLOCK或_IGNORE_BLOCK一种。当内存释放时,释放函数根据全局变量_crtDbgFlag的取值和内存块的类型决定执行何种操作:
1. 不检查。如果内存块类型为_IGNORE_BLOCK直接释放,因为这个标志指示无须监视这个内存块的运行情况。
2. 检查_NORMAL_BLOCK、_CRT_CLIENT_BLOCK内存块是否有越界的情况,要求_crtDbgFlag的_CRTDBG_CHECK_ALWAYS_DF标志位置位。如果内存块上下两个隔离带(gap)之一取值不为0xfd,则说明操作中有越界发生,就报告一个堆损坏情况。
3. 另一种可以捕捉的错误是对“野指针”的检查。野指针是指当前已释放的指针,但在以后某个时刻还仍在使用的指针。要求_crtDbgFlag的_CRTDBG_DELAY_FREE_MEM_DF标志位置位。此时内存释放函数会将这片内存区置为0xDD。如果再次使用这片内存,则在下次释放时会报告一个错误。
4. 检查运行时库的内存管理。方法类似于2和3。
5. 检查内存泄漏。要求标志位_CRTDBG_LEAK_CHECK_DF置位。当运行时库退出时,它检查运行时库的堆,并将所有未移除的内存块信息倾印出来。
这里解释一下这个输出。输出给出了分配这块内存时的文件名和行号,双击它即可定位到显示源代码。括在{}号的中数字是内存分配函数调用的次数,内存分配函数每调用一次,这个数字增1。在{}后面紧接着是块类型和内存的起始地址,大小,并输出了内存区前12个字节。
二 运行时库与IDE环境的连接
到目前为止,我们讨论内存分配函数时,还只提到了_malloc_dbg这个我们几乎从不在代码中使用的函数。为清楚起见,显然需要知道new这样的c++关键字和malloc这样的运行时函数怎样映射到_malloc_dbg上。
首先,在Debug版下,malloc函数只能分配_NORMAL_BLOCK类型的内存,它并不记录分配内存的那行代码所在的文件名和行号。如果有内存问题时,查错就会比较麻烦。但是,如果定义了标识符_CRTDBG_MAP_ALLOC,则malloc函数就会影射为_malloc_dbg函数,并且通过预处理标识符__FILE__和__LINE__来得到代码所在的文件名和行号。这里只讲了malloc函数,其它内存分配函数和内存回收函数也是一样的。
当_DEBUG定义时,new被首先宏替换为DEBUG_NEW,经预处理后最后被替换为void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);这里的lpszFileName和nLine是由__FILE__和__LINE__传入的常量,最后在new中仍然后调用_malloc_dbg函数。
另外一个问题,在MFC程序中如何设置_crtDbgFlag变量。实际上,在MFC程序中我们使用宏afxMemDF来代替_crtDbgFlag变量。MFC并没有象运行时库那样提供函数来操纵这个变量。因此只能直接修改afxMemDf的值。
这样一来,由于我们主要在程序中使用上述两种方式分配堆内存,因此我们分配的内存都得到了监视。
现在,只需要将运行时库的各种诊断信息输出重定位到IDE的output窗口就行了。限于篇幅,不在这里讲述技巧,只要知道运行时库提供了相关的重定位函数,允许把调试字符串输出到调试器就可以了。
1 内存分配断点
有了这些知识,现在我们来看一种新的断点,内存分配断点。在图 0?10中的内存泄漏诊断输出中,你可以通过双击输出条目定位到分配内存的语句,但这仍不够精确。也许这条语句会执行很多次,但只有其中的几次才导致内存泄漏,问题是怎么才知道是什么条件下分配的内存块导致了泄漏,这样才能及时中断程序的运行,并查看一些重要的变量取值。内存断点提供了这种可能。先在output窗口中观察到造成内存泄漏的分配次数,你可以在watch窗口中加入变量_crtBreakAlloc,令其值等于这个次数,则当程序运行时,分配次数达到这个值时程序会中断。如果你使用多线程的运行时库,则应该使用{,,msvcrtd.dll}_crtBreakAlloc以代替_crtBreakAlloc。另一种方法是使用运行时库函数_CrtSetBreakAlloc。这个函数在调试版中作用跟上面的方法相同,但在发行版中会被注释掉,从而不影响程序运行。
2 内存检查点
如果打开了内存分配监视,则还可以设置内存检查点,以精确地定位内存泄漏。通常,运行时库只在退出时才输出诊断信息,这个诊断信息是全局的,包含了一次会话期间所有分配发生泄漏的情况。有时候仅仅需要对某段代码进行诊断。这时可以通过设置内存检查点,比较前后两次内存检查点的差来诊断内存泄漏情况。运行时库提供了_CrtMemCheckpoint获取内存快照,提供函数_CrtMemDifference来判断是否存在泄漏,提供函数_CrtMemDumpStatistics来输出诊断信息。MFC则提供了类CMemoryState来完成诊断。内存检查点使用十分简单,可参照MSDN。
3 避免内存泄漏的方法
一些内存泄漏的产生不是由于你没有注意到应该回收它,而是因为发生了程序跳转,比如发生了一个异常,从而绕开了你的正常执行路径,本来你是准备在那里回收它的。
可以使用智能指针来包装你的指针,确保在异常情况发生时,也可以回收内存。
- 06-30· 引用 (原创)陌上花.赏菊
- 06-21· “感动南京”人物谢二喜
- 06-21· 男士服饰搭配的基本原则
- 06-21· 程式内衣简介
- 06-21· 搭配点评 无论你身材、肤
- 06-21· 品牌内衣
- 06-21· 红脸蛋与绿西瓜
- 06-19· [神马]【2011-03-03】外贸童
- 06-19· 济南小商品 济南大明湖东
- 06-19· 妒
- 06-19· 2011年03月24日
- 06-19· 一个小小的纹身
- 06-19· 女装,女鞋,超值店
- 06-19· 谈谈购房体会
- 06-19· [转载]中医肾病用药体会
- 06-19· 我的读书心得体会
- 06-19· [转载]学习精细化管理写了
- 06-19· 谈谈拜《楞严经》的体会
- 06-18· 上海基本药物增补高价外
- 06-18· 辉瑞与百时美施贵宝叫停