内存泄漏与检测
动态分配的内存在程序结束后而一直未释放,就出现了内存泄漏。
一般常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显式释放的内存。
应用程序一般使用malloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,
否则,这块内存就不能被再次使用,就说这块内存泄漏了。
接着来分析下面的C代码:
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
请问运行Test 函数会有什么样的结果?
分析:上面的代码试图使用指针作为参数,分配动态内存。该代码会存在两个问题:
1. 内存泄漏。
首先,通过指针作为参数无法成功申请一块动态分配的内存。这是因为,GetMemory()函数获得的是实参指针变量的一个拷贝。因此,它只是将新分配的内存赋给了形参(即实参指针的拷贝)。而实参并没有获得这块内存。在Test()函数中,发现并没有释放str指向内存的语句。但这不是内存泄露的根本原因。即使在程序后面加上一句:
free(str);
内存依然会泄漏。这是因为,str根本没有获得这块内存,而是由形参获得了。而形参是一个栈上的变量。在函数执行之后就已经被系统收回了。也就是说,有一块内存在分配之后,被遗忘了,这是造成了内存泄漏的根本原因。 要想成功获得分配的内存,可以采用下面的两种方法:
①
char* GetMemory(char *p)
{
p = (char *)malloc(100);
return p;
}
上面的代码直接返回新分配的内存地址的指针。由于内存是在堆上而不是在栈上分配的,所以函数返回后不存在任何问题。
②
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
这种方法是通过指针的指针来分配内存。用这种方法分配内存,传递给函数的是指针地址的一个拷贝,那么*p就是指针本身。 因此新分配的内存成功的赋给了做实参的指针。
2. NULL指针引用导致程序崩溃。
由于str并没有获得这块内存,那么str的值依然为NULL,所以strcpy()函数访问了一个NULL指针,直接导致程序崩溃。
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
请问运行Test 函数会有什么样的结果?
分析:上面的代码使用指针的指针来分配内存,str会成功获得分配的内存。 但是,该题在使用了指针后,却忘记了对内存的释放。所以应该在后面加上:
free(str);
str = NULL;
内存泄漏本身影响并不大,一次普通的内存泄漏用户根本感觉不到内存泄漏的存在。真正影响大的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。因此,在平时编码之时应该提高警惕,在使用完动态分配的内存之后,及时释放掉。
那么如何防止内存泄漏呢?内存分配应该遵循下面的原则:
1. 谁分配,谁释放。在写下new/malloc时,要马上写下配对的delete/free以此释放掉。
2. 出错处理需释放。在函数错误处理分支中,记得释放掉已经分配的内存。
为了应对这种复杂的出错处理逻辑,避免一不小心就忘记了释放分配的资源,可以采用出错处理模块化处理, 在函数的尾部增加错误处理模块。一旦出错,就利用goto语句跳转到出错处理模块集中处理出错情况下资源的释放。 此外,还可以用SHE __try__leave__except结果化异常处理机制来处理系统中的异常的发生时资源的释放。
3. 网络上拷贝的代码,要仔细检查内存使用情况,预防内存泄漏。
4. 对于复杂指针的使用,如果做不到“谁分配,谁释放”,那么可以使用引用计数来管理这块内存的使用。 引用计数方式来管理内存,即在类中增加一个引用计数,跟踪指针的使用情况。当计数为0了,就可以释放指针了。 此种方法适合于通过一个指针申请内存之后,会经过程序各种复杂引用的情况。
下面是一个实际例子:
class CXData
{
public:
CXData()
{
m_dwRefNum = 1; //引用计数赋初值
}
ULONG AddRef() //增加引用
{
ULONG num = InterlockedIncrement(&m_dwRefNum);
return num;
}
ULONG Release() //减少引用
{
ULONG num = InterlockedDecrement(&m_dwRefNum);
if(num == 0) //当计数为0了,就释放内存
{
delete this;
}
return num;
}
private: ULONG m_dwRefNum; //引用计数
}
使用实例:
CXData *pXdata = new CXData;
pXdata->AddRef(); //使用前增加计数
pXdata->Release(); //使用后减少计数,如果计数为零,则释放内存
此外,内存泄漏还可能由调用了不正确的系统API而造成。比如在Windows里的CreateThread函数。
CreateThread:是Windows的API函数(SDK函数的标准形式,直截了当的创建方式,任何场合都可以使用),提供操作系统级别的创建线程的操作,且仅限于工作者线程。
不调用MFC和RTL的函数时,可以用CreateThread,其它情况不要使用。因为:
C Runtime中需要对多线程进行纪录和初始化,以保证C函数库工作正常。 MFC也需要知道新线程的创建,也需要做一些初始化工作。
有些CRT的函数象malloc(),fopen(),_open(),strtok(),ctime(),或localtime()等函数需要专门的线程存储局部的数据块,这个数据块通常需要在创建线程的时候就建立,如果使用CreateThread,这个数据块就没有建立,但函数会自己建立一个,然后将其与线程联系在一起,这意味着如果你用CreateThread来创建线程,然后使用这样的函数,会有一块内存在不知不觉中创建,而且这些函数并不将其删除,而CreateThread和ExitThread也无法知道这件事,于是就会有Memory Leak,在线程频繁启动的软件中,迟早会让系统的内存资源耗尽。
思考题:
如何设计一个跨平台的内存泄漏检测机制?也就是说,如果在不同的平台发生了内存泄漏,我们如何用统一的方法检测它?