内存泄漏与检测
从堆上分配内存,如果不注意释放,就会产生内存泄漏。那么如何分配内存才符合科学的规范呢?下面来看一段内存分配算法:
char *pDest = (char *)malloc(4096);
free(pDest);
//下面是用typedef定义一个新结构最常用的定义形式
//在微软的面试中,在考查你某个算法前,一般会让你先定义一个与算法相关的结构。
//比如链表排序的时候,让你定义一个链表的结构。
typedef struct _node
{
int value;
struct _node * next;
}node, *link;
node *pnode = NULL; //声明变量都应该初始化,尤其是指针
pnode = (node *)malloc(sizeof (node)); //内存分配
//务必检测内存分配失败情况,程序健壮性的考查
//加上这样的判断语句,会让你留给面试官一个良好的印象
//不加这样的判断,如果分配失败,会造成程序访问NULL指针崩溃
if (pnode == NULL)
{
//出错处理,返回资源不足错误信息
}
memset(pnode, 0, sizeof(node)); //新分配的内存应该初始化,否则内存中含有无用垃圾信息
pnode->value = 100;
printf(“pnode->value
= %d\n”,
pnode->value);
node * ptmp = pnode;
ptmp += 1; //指针支持加减运算,但须格外小心
free(pnode); //使用完内存后,务必释放掉,否则会泄漏。一般采取谁分配谁释放原则
pnode = NULL;//释放内存后,需要将指针置NULL,防止野指针
动态分配的内存在程序结束后而一直未释放,就出现了内存泄漏。一般常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用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(void)
{
char *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. 出错处理需释放。在函数错误处理分支中,记得释放掉已经分配的内存。
3. 网络上拷贝的代码,要仔细检查内存使用情况,预防内存泄露。
比如下面的这段代码就忘记了在错误处理的地方释放前面分配的内存:
NTSTATUS QueryObjectName(HANDLE h)
{
int ret;
char *str
= "Hello, how are u?";
char *pstr
= NULL;
NTSTATUS st;
pstr = (char *)malloc(256);
if (pstr == NULL)
{
printf("No more memory\n");
return;
}
strncpy(pstr, str, strlen(str));
pstr[strlen(str)]
= '\0';
char *namebuf = (char *)malloc(1024);
//注意下面的出错处理代码,忘记了释放pstr所指内存资源
if (buf == NULL)
{
printf("No more memory\n");
return;
}
st = NtQueryObject(h, FileNameInformation, namebuf,
ret, NULL);
//注意下面的出错处理代码,忘记了释放pstr和namebuf所指内存资源
if (!NT_SUCCESS(st))
{
return st;
}
...
free(buf);
free(pstr);
return st;
}
下面的代码在出错处理的地方增加了内存释放的代码,防止了内存的泄漏。
NTSTATUS QueryObjectName(HANDLE h)
{
int ret;
NTSTATUS st
= STATUS_UNSUCCESSFUL;
char *str
= "Hello, how are u?";
char *pstr
= (char *)malloc(256);
if (pstr == NULL)
{
printf("No more memory\n");
return st;
}
strncpy(pstr, str, strlen(str));
pstr[strlen(str)]
= '\0';
char
*namebuf = (char *)malloc(1024);
if (buf == NULL)
{
printf("No more memory\n");
//错误发生后,及时的释放了pstr内存
free(pstr);
return st;
}
st = NtQueryObject(h, FileNameInformation, namebuf,
ret, NULL);
if (!NT_SUCCESS(st))
{
//错误发生后,及时的释放了buf与pstr的内存
free(buf);
free(pstr);
return st;
}
...
free(buf);
free(pstr);
return st;
}
为了应对这种复杂的出错处理逻辑,避免一不小心就忘记了释放分配的资源,可以采用出错处理模块化处理,在函数的尾部增加错误处理模块。一旦出错,就利用goto语句跳转到出错处理模块集中处理出错情况下资源的释放。
NTSTATUS QueryObjectName(HANDLE h)
{
int ret;
NTSTATUS st;
char *str
= "Hello, how are u?";
char *pstr
= (char *)malloc(256);
if (pstr == NULL)
{
printf("No more memory\n");
goto Error;
}
strncpy(pstr, str, strlen(str));
pstr[strlen(str)] = '\0';
char *namebuf = (char *)malloc(1024);
if (buf == NULL)
{
printf("No more memory\n");
goto Error;
}
st = NtQueryObject(h, FileNameInformation, namebuf,
ret, NULL);
if (!NT_SUCCESS(st))
{
printf("No more memory\n");
goto Error;
}
...
Error:
//发生错误后,统一处理内存释放问题
if (buf)
{
free(buf);
}
if (pstr)
{
free(pstr);
}
return st;
}
此外,还可以用SHE __try__leave__except结果化异常处理机制来处理系统中的异常的发生时资源的释放。
NTSTATUS QueryObjectName(HANDLE h)
{
int ret;
NTSTATUS st;
char *str
= "Hello, how are u?";
char *pstr
= (char *)malloc(256);
__try
{
if (pstr == NULL)
{
printf("No more memory\n");
__leave;
}
strncpy(pstr, str, strlen(str));
pstr[strlen(str)]
= '\0';
char
*namebuf = (char *)malloc(1024);
if (buf == NULL)
{
printf("No more memory\n");
__leave;
}
st = NtQueryObject(h, FileNameInformation, namebuf,
ret, NULL);
if (!NT_SUCCESS(st))
{
printf("No more memory\n");
__leave;
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
st = GetExceptionCode();
}
if (buf)
{
free(buf);
}
if (pstr)
{
free(pstr);
}
return st;
}
为了管理使用复杂的指针申请的内存的释放,可以利用智能指针或者利用引用计数的方式来管理申请到的内存。下面介绍一下引用计数的方法:
引用计数方式来管理内存,即在类中增加一个引用计数,跟踪指针的使用情况。当计数为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(); //使用后减少计数,如果计数为零,则释放内存
如果内存已经发生了泄漏,有什么方法来检测内存的泄漏呢?比如设计一个方法,来实现跨平台的内存泄漏检测:
方法就是定义一个自己的内存分配与释放的函数:
1,char *mymalloc(n)
{
char *p = (char *)malloc(n);
v.push_back(p);
return (void *)p;
}
当调用malloc()函数分配好了内存后,将p放入一个容器(比如C++里的vector就是一个容器)里。
2,void myfree(p)
{
v.pop_back(p);
free(p);
}
在释放内存的时候,将该指针从这个容器里删除。这样当程序退出的时候,如果容器v里非空,那么一定是发生了内存泄漏。
3,void myrealloc(p,size)
{
void *p1 = realloc(p,size);
if(p1==p)
{
}
else
{
v.pop_back(p);
v.push_back(p1);
}
return p1;
}
对于realloc()函数需要做特殊处理,因为realloc()函数返回的指针可能是输入的指针,也可能是重新分配的内存指针。如果是输入的指针,则不用再加入容器;否则需要删除之前放入容器的指针,把新的指针放入容器中。