内存泄露
什么是内存泄露
程序中申请的资源在程序进程退出之后系统都会回收,所以只要程序退出,程序申请的内存都会被释放。但是,如果程序在运行的过程中不停的申请内存(或资源),而且不再使用时不进行释放,会导致系统资源耗尽。
内存泄露实例代码
void leakfunc()
{
char*szBuf = new char[256];
memset(szBuf, 0, sizeof(char) * 256);
strcpy(szBuf, "Teststring\n");
printf(szBuf);
}
例如上述函数分配了一块256byte的内存,但是没有释放,在调用到该函数时会导致内存泄露。
如何检测内存泄露
1. 使用工具检测
检测内存泄露的工具不少,例如DevPartner(BounderChecker) , PurifyPlus等,使用工具检测是最简单也是最有效的方法。
PurifyPlus是异常强大的程序白盒测试工具,基本上所有的内存问题都可以测试,内存泄露,访问未初始化的内存,野指针,缓冲区溢出等。
2. Windows 提供的内存泄露检测接口
Windows提供了内存泄露的检测接口_CrtDumpMemoryLeaks()(头文件为crtdbg.h)。
例如我调用上述有内存泄露的接口,代码如下:
int _tmain(intargc, _TCHAR*argv[])
{
leakfunc();
_CrtDumpMemoryLeaks();
return0;
}
运行结束之后Output窗口会输出如下Log:
蛋疼,是不是发现只检测到泄露了多少内存,但是没有统计内存泄露的位置。那下面我们就让VS把内存泄露的位置也输出出来。
重新定义new 和 malloc, 定义如下
#define new new(_CLIENT_BLOCK, __FILE__,__LINE__)
#define malloc _malloc_dbg(_CLIENT_BLOCK, __FILE__,__LINE__)
再运行,输出如下:
是不是发现已经正确的显示了行号以及文件。
使用Windows提供的接口也比较方便,但是有很多不足,例如如果是已经封装好的库里面内存泄露则没有办法统计内存泄露的位置,除非你也将第三方的库用自己重新定义的malloc和new重新编译一次。
3. 自己统计内存泄露
如果有人说我是在嵌入式平台上运行的,不像Windows一样有工具,也没有提供内存检查的接口,如何检测内存泄露呢。其实是有办法的,但是很复杂。我做一个简单的实现。
---------------------------------------memoryleak.h------------------------------------------
#ifndef __MEMORYLEAK_H__
#define __MEMORYLEAK_H__
void * operator new[](size_t size, const char* file, int line);
void * operator new(
size_t size,
const char * file,
int line
);
void operator delete[] (void* addr);
void operator delete(void* addr);
void* mymalloc(size_t size,
const char* file,
int line);
void myfree(void* addr);
void dumpMemoryLeak();
void initDumpMemoryLeak();
void deinitDumpMemoryLeak();
#endif
--------------------------------memoryleak.cpp------------------------------
#include "stdafx.h"
#include "memleak.h"
#include <malloc.h>
#include <string.h>
#include <assert.h>
typedef struct LEAK_NODE_tag
{
char file[260];
int line;
int size;
void* address;
LEAK_NODE_tag* next;
LEAK_NODE_tag* prev;
}LEAK_NODE_T;
static LEAK_NODE_T m_head = {0};
void * operator new[](size_t size, const char* file, int line)
{
return mymalloc(size, file, line);
}
void * operator new(
size_t size,
const char * file,
int line
)
{
return mymalloc(size, file, line);
}
void operator delete[] (void* addr)
{
return myfree(addr);
}
void operator delete(void* addr)
{
return myfree(addr);
}
void* mymalloc(size_t size,
const char* file,
int line)
{
if (size == 0)
{
return NULL;
}
void* memory = NULL;
LEAK_NODE_T* node = NULL;
do
{
void* memory = malloc(size);
if (!memory)
{
break;
}
node = (LEAK_NODE_T*) malloc(sizeof(LEAK_NODE_T));
if (!node)
{
break;
}
memset(node, 0, sizeof(LEAK_NODE_T));
if (file)
{
strcpy(node->file, file);
}
node->line = line;
node->size = size;
node->address = memory;
m_head.prev->next = node;
node->next = &m_head;
node->prev = m_head.prev;
m_head.prev = node;
return memory;
} while (0);
if (node)
{
free(node);
}
if (memory)
{
free(memory);
}
return NULL;
}
void myfree(void* addr)
{
if (!addr)
{
return;
}
LEAK_NODE_T* node = m_head.prev;
while (node != &m_head)
{
if (node->address == addr)
{
node->next->prev = node->prev;
node->prev->next = node->next;
free(node);
free(addr);
return;
}
node = node->prev;
}
assert(0); // this addr is invalid or not malloc via dump memory
free(addr);
}
void dumpMemoryLeak()
{
LEAK_NODE_T* node = m_head.next;
if (node != &m_head)
{
printf("Detected memory leaks! \n");
}
while (node != &m_head)
{
printf("%s(%d) : block at %p, %d bytes long\n", node->file, node->line, node->address, node->size);
node = node->next;
}
}
void initDumpMemoryLeak()
{
m_head.prev = &m_head;
m_head.next = &m_head;
}
void deinitDumpMemoryLeak()
{
LEAK_NODE_T* node = m_head.next;
LEAK_NODE_T* next = NULL;
while (node != &m_head)
{
next = node->next;
free(node);
node = next;
}
}
-------------------memorytest.cpp--------------------
void leakfunc();
#define new new(__FILE__, __LINE__)
#define malloc(size) mymalloc(size, __FILE__, __LINE__)
#define free(addr) myfree(addr)
void leakfunc()
{
char* szBuf = new char[256];
memset(szBuf, 0, sizeof(char) * 256);
strcpy(szBuf, "Test string\n");
printf(szBuf);
double* pdouble = (double*)malloc(sizeof(double));
int *pint = (int*)malloc(sizeof(int));
free(pdouble);
}
int _tmain(int argc, _TCHAR* argv[])
{
initDumpMemoryLeak();
leakfunc();
dumpMemoryLeak();
deinitDumpMemoryLeak();
return 0;
}
上述例子输入如下结果:
从上图可以看出该实现正确的统计了程序的内存泄露。
该例子只是一个非常简单的实现,在一些更低级的平台上,基于防止内存碎片等考虑,程序可能需要使用自己的内存池,这时可以在自己分配内存的时候添加Overhead来统计分配的信息。
常用的防止内存泄露的方案
1. 合理的编码规范防止内存泄露
如:
a) do while{0} 以及goto语句的合理使用
例如:
Void func()
{
A* a = NULL;
B* b = NULL;
Do
{
a = new A();
if (!a)
{
break;
}
b = new B();
if (!b)
{
break;
}
…
}while(0);
if (a)
{
delete a;
}
if (b)
{
delete b;
}
}
b) 在设计类时在init函数分配资源,在析构函数释放资源等
2. 使用引用计数
对于指针需要在很多模块传递的情况,使用引用计数可以很方便的防止内存泄露。使用的原则是当调用者需要管理这个对象时调用该对象的retain, 当其不需要管理时调用release。当然,如果用户错误的调用retain和release,就会引发严重的错误(内存泄露或者程序崩溃)。
3. 智能指针
4. void leakfunc()
5. {
6. char* szBuf = new char[256];
7.
8. memset(szBuf, 0, sizeof(char) * 256);
9.
10. strcpy(szBuf, "Teststring\n");
11.
12. printf(szBuf);
13.
14. double* pdouble = (double*)malloc(sizeof(double));
15.
16. auto_ptr<int> pint (new int(5));// this will not cause memory leak
17.
18. free(pdouble);
19. }
但是,智能指针不能用于数组。
非法内存访问
在C/C++程序中,非法内存使用是永恒的话题。内存非法访问一般包括如下形式:
1. 使用NULL指针。
例如 A* a = NULL; a->func();
2. 使用野指针。
例如 A* a = new A; delete a; a->func();
3. Double free。
例如A* a = new A; delete a;delete a;
4. 缓冲区溢出
例如 char szBuf[4] = {0}; strcpy(szBuf, “Helloworld”);
非法内存访问的后果比较严重,而且调试起来非常麻烦,所以尽量在写代码的时候多小心,否则调试的时候要崩溃。
VS断点支持
调试内存问题使用需要使用数据断点,我就简单的讲一下VS支持的常见断点类型。
- 普通断点
这个我想大家都会用,就是在某一行按F9, 运行到该行时程序会进入调试模式。
- 条件断点
可以对普通断点设置条件,设置条件断点的方式为,在设置断点的那一行右击,选择“断点”->“条件”,然后就可以设置断点起效的条件。
- 跟踪点(命中条件)
对于在程序运行过程中需要输出Log,而又不想添加输出Log代码的话,可以使用跟踪点,添加跟踪点得方法为在已设置普通断点的那一行右击,选择“断点”->“命中条件”;或者在未设置断点的某行右击,选择“断点”->“插入跟踪点”。
- 数据断点(内存断点)
内存断点是解决非法内存访问最有效的武器。
插入数据断点的方法为点击“调试”->“新建断点”->“新建数据断点”,然后指定需要监视的内存的位置以及大小(只支持1byte, 2 byte, 4byte)。设置断点后,在这块内存的值修改后,系统会进入调试状态。
非法内存访问调试
1. 空指针
空指针是最好调试的,在系统访问空指针(或者系统内存区域时),系统一般会进入调试状态,可以直接看到当前的堆栈,所以一般很好定位。
但是在Windows程序下需要注意,Windows 通过一个类的空指针去访问一个函数可能和你预料的有一些不一样。例如定义如下类:
class A
{
public:
A()
{
m_value= 0;
}
voidsetValue(intvalue)
{
m_value= value;
}
voidPrintValue()
{
printf("Value is %d\n", m_value);
}
voidPrintNull()
{
printf("Just print log\n");
}
private:
intm_value;
};
然后使用如下调用方式:
A*a = NULL;
a->PrintNull(); (1) 程序运行正常
a->setValue(5); (2) 程序死机
a->PrintValue();(3) 程序死机
上面三个都是从对象的空指针调用对象的成员函数,但是(1)不会死机,而(2)(3)会死机,这是为什么呢?这就要涉及到编译器怎么实现类的成员函数,类的成员函数和普通的C函数是一样的,只是编译器在编译的时候给它改了一个名字,同时加了一个参数(第一个参数)表示对象的this指针,例如PrintNull就会变位类似XXXX_PrintNULL_XX(A* a)的形式。而PrintNull函数不引用对象里面的任何成员变量,所以不会导致非法内存访问,因此就没有事情。而(2)和(3)都需要访问对象里面的成员变量,这样就访问了非法的内存,所以会死机。而且大家会发现程序会死在setValue和PrintValue里面访问成员变量的位置。所以如果以后遇到类似的问题,基本都是因为调用成员函数的对象指针为空。
2. 野指针访问
野指针问题是C/C++最难调试的问题之一,我觉得只能看个人的经验以及人品了。我们来分析一下野指针会导致哪些灾难性的后果。
(1) 程序访问到野指针的地方宕机。这个已经是最好的结果了,至少你能保留死机现场,知道哪死机的,能调试。
(2) 通过野指针把别的正常数据改了,导致别人运行不正常。
(3) 通过野指针把别的指针改了,死在别的模块。
分享一下野指针的调试办法,最常用的办法就是通过内存断点了。如果你知道每次都是某个对象的某个成员被改,你就可以设置内存断点来监视这段内存,当程序通过野指针来修改这段内存的值时,就会被逮个正着。如果每次的位置都是随机的,这个我不会,只能通过分析代码或者是Revert代码看是哪一个Changlist出问题的。
应该在编程中避免野指针的出现,需要养成一些好的习惯,简单来讲就是:
(1) retain和release合理的使用。(需要用时retain,不需要使用了release)。
(2) delete 和 release之后将指针置空。
(3) 使用智能指针
3. double free
double free的问题在Windows上会稍微好查一些,因为Windows在调试模式下会对内存做检查,如果double free 会Assert。
上述的稍微好检查一点只是相对的,例如在使用cocos2d-x的时候,经常会遇到由于引用计数没有管理好而导致的double free。
这调用堆栈中你不知道到被double free的是哪个对象,而且也很难跟踪这个对象在哪被释放了。
很难被跟踪有如下的原因:
(1) 不知道对象的类型,因为已经被free过,所以vtbl相关信息都已经被清除了。
(2) 没办法在析构函数设断点跟踪,因为这种类型的对象很多,根本没有办法跟踪。
(3) 内存断点也不是很好设置,因为在大部分情况下,每次内存位置都不一样。
但是这种办法可以采用如下方式:
方法一,可以检查析构时的代码,肯定有某个对象retain和release未匹配。
方法二,可以在当前场景的析构函数开始处设置断点,然后再设置内存断点。虽然对象在内存中的位置可能会变化,但是在一个静态的界面中,在CCArray中的索引变化可能性不大,所以可以在释放之前获取该索引对应的对象的地址,再设置内存断点。
在开发工作中预防double free和防止野指针基本类似,但是要多注意一点:如果你对外发布的库提供了创建对象/数据的接口,也应该提供删除对象/数据的接口。因为:
(1) 用户不知道你是用什么库函数进行对象分配的(new, new[], malloc /Debug 版 / Release版)。
(2) 如果分配和释放的接口匹配会导致问题。
4. 缓冲区溢出
缓冲区溢出也是C/C++程序中永恒的话题,据说80%的系统漏洞都是由缓冲区溢出引起的。
在栈和堆里面的缓冲区溢出都会导致严重的后果。目前的调试方法还是使用内存断点。
缓冲区溢出应该在编程时注意避免:
1. 不要使用strcpy, 而使用strncpy。