针,而现在传入 operator delete 的却不是,所以就无法在全局对象(appMemory)所记录的数据中找到相应的内存分配信息。
图3
综上所述,当 new 和 delete 的调用形式不匹配时,由于程序有可能崩溃或者内存子系统找不到相应的内存分配信息,在程序最终打印出 "ErrorDelete" 的方式只能检测到某些"幸运"的不匹配现象。但我们总得做点儿什么,不能让这种危害极大的错误从我们眼前溜走,既然不能秋后算帐,我们就实时输出一个 warning 信息来提醒用户。什么时候抛出一个 warning 呢?很简单,当我们发现在 operator delete 或 operator delete[] 被调用的时候,我们无法在全局对象(appMemory)的 map 中找到与传入的指针值相对应的内存分配信息,我们就认为应该提醒用户。
既然决定要输出warning信息,那么现在的问题就是:我们如何描述我们的warning信息才能更便于用户定位到不匹配删除错误呢?答案:在 warning 信息中打印本次 delete 调用的文件名和行号信息。这可有点困难了,因为对于 operator delete 我们不能向对象 operator new 一样做出一个带附加信息的重载版本,我们只能在保持其接口原貌的情况下,重新定义其实现,所以我们的 operator delete 中能够得到的输入只有指针值。在 new/delete 调用形式不匹配的情况下,我们很有可能无法在全局对象(appMemory)的 map 中找到原来的 new 调用的分配信息。怎么办呢?万不得已,只好使用全局变量了。我们在检测子系统的实现文件中定义了两个全局变量(DELETE_FILE,DELETE_LINE)记录 operator delete 被调用时的文件名和行号,同时为了保证并发的 delete 操作对这两个变量访问同步,还使用了一个 mutex(至于为什么是 CCommonMutex 而不是一个 pthread_mutex_t,在"实现上的问题"一节会详细论述,在这里它的作用就是一个 mutex)。
| char DELETE_FILE[ FILENAME_LENGTH ] = {0};
int DELETE_LINE = 0;
CCommonMutex globalLock;
|
而后,在我们的检测子系统的头文件中定义了如下形式的 DEBUG_DELETE
| extern char DELETE_FILE[ FILENAME_LENGTH ];
extern int DELETE_LINE;
extern CCommonMutex globalLock;//在后面解释
#define DEBUG_DELETE globalLock.Lock(); \
if (DELETE_LINE != 0) BuildStack();\ (//见第六节解释)
strncpy( DELETE_FILE, __FILE__,FILENAME_LENGTH - 1 );\
DELETE_FILE[ FILENAME_LENGTH - 1 ]= '\0'; \
DELETE_LINE = __LINE__; \
delete
|
在用户被检测文件中原来的宏定义中添加一条:
| #include "MemRecord.h"
#if defined( MEM_DEBUG )
#define new DEBUG_NEW
#define delete DEBUG_DELETE
#endif
|
这样,在用户被检测文件调用 delete operator 之前,将先获得互斥锁,然后使用调用点文件名和行号对相应的全局变量(DELETE_FILE,DELETE_LINE)进行赋值,而后调用 delete operator。当 delete operator 最终调用我们定义的 operator delete 的时候,在获得此次调用的文件名和行号信息后,对文件名和行号全局变量(DELETE_FILE,DELETE_LINE)重新初始化并打开互斥锁,让下一个挂在互斥锁上的 delete operator 得以执行。
在对 delete operator 作出如上修改以后,当我们发现无法经由 delete operator 传入的指针找到对应的内存分配信息的时候,就打印包括该次调用的文件名和行号的 warning。
天下没有十全十美的事情,既然我们提供了一种针对错误方式删除的提醒方法,我们就需要考虑以下几种异常情况:
1. 用户使用的第三方库函数中有内存分配和释放操作。或者用户的被检测进程中进行内存分配和释放的实现文件没有使用我们的宏定义。 由于我们替换了全局的 operator delete,这种情况下的用户调用的 delete 也会被我们截获。用户并没有使用我们定义的DEBUG_NEW 宏,所以我们无法在我们的全局对象(appMemory)数据结构中找到对应的内存分配信息,但是由于它也没有使用DEBUG_DELETE,我们为 delete 定义的两个全局 DELETE_FILE 和 DELETE_LINE 都不会有值,因此可以不打印 warning。
2. 用户的一个实现文件调用了 new 进行内存分配工作,但是该文件并没有使用我们定义的 DEBUG_NEW 宏。同时用户的另一个实现文件中的代码负责调用 delete 来删除前者分配的内存,但不巧的是,这个文件使用了 DEBUG_DELETE 宏。这种情况下内存检测子系统会报告 warning,并打印出 delete 调用的文件名和行号。
3. 与第二种情况相反,用户的一个实现文件调用了 new 进行内存分配工作,并使用我们定义的 DEBUG_NEW 宏。同时用户的另一个实现文件中的代码负责调用 delete 来删除前者分配的内存,但该文件没有使用 DEBUG_DELETE 宏。这种情况下,因为我们能够找到这个内存分配的原始信息,所以不会打印 warning。
4. 当出现嵌套 delete(定义可见"实现上的问题")的情况下,以上第一和第三种情况都有可能打印出不正确的 warning 信息,详细分析可见"实现上的问题"一节。
你可能觉得这样的 warning 太随意了,有误导之嫌。怎么说呢?作为一个检测子系统,对待有可能的错误我们所采取的原则是:宁可误报,不可漏报。请大家"有则改之,无则加勉"。
5.动态内存泄漏信息的检测
上面我们所讲述的内存泄漏的检测能够在程序整个生命周期结束时,打印出在程序运行过程中已经在堆上分配但是没有释放的内存分配信息,程序员可以由此找到程序中"显式"的内存泄漏点并加以改正。但是如果程序在结束之前能够将自己所分配的所有内存都释放掉,是不是就可以说这个程序不存在内存泄漏呢?答案:否!在编程(www.cppentry.com)实践中,我们发现了另外两种危害性更大的"隐式"内存泄漏,其表现就是在程序退出时,没有任何内存泄漏的现象,但是在程序运行过程中,内存占用量却不断增加,直到使整个系统崩溃。
1. 程序的一个线程不断分配内存,并将指向内存的指针保存在一个数据存储中(如 list),但是在程序运