C++内存泄露和内存管理

1   内存泄露

指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。 

有两种类型的内存泄漏。

 

1.1 堆内存泄漏(Heap leak)

堆内存指的是程序运行中根据需要分配通过malloc,reallocnew等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.

C++内存泄露和内存管理_第1张图片


1.2系统资源泄露(Resource Leak)

主要指程序使用系统分配的资源比如Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定

下面简单介绍两种判断句柄泄露的方法:

1) 打开任务管理器:选择菜单:查看—选择列,勾上“句柄数“此时任务管理器中多了一列句柄数,如果你发现一个进程句柄数在不断增加,那么可能该进程就存在内存泄露了

C++内存泄露和内存管理_第2张图片

2) 使用工具Process Explorer,该工具能够非常明了的看到进程所正在使用的内核对象,当存在句柄对象时,它能够协助你分析找到原因,选择View —> Lower Pane View —> DLLs (或Handles)

C++内存泄露和内存管理_第3张图片

该程序在访问1.txt这个文件的时候,没有关闭句柄,导致文件打开时句柄不断增加。

 

2   内存泄露危害:

最直接的危害就是系统内存耗尽,系统死机。内存泄漏是最难发现的常见错误之一,因为除非用完内存或调用malloc失败,否则都不会导致任何问题。如果程序运行时间足够长,如后台进程运行在服务器上,只要服务器不宕机就一直运行,一个小小的失误也会对程序造成重大的影响,如造成某些关键服务失败。

 

3 内存泄露检测方法:

在Windows平台下,检测内存泄漏的方法主要有三两种,MS C-Runtime Library内建的检测功能;插件式的外部检测工具;性能检测工具分析系统内存泄露

 

3.1 MS C-Runtime Librarycrt(dbg.h中的api)

(内容来自http://www.cnblogs.com/skynet/archive/2011/02/20/1959162.html#)

 

3.1.1 检测是否存在内存泄漏问题

Windows平台下面Visual Studio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法,原理大致如下:内存分配要通过CRT在运行时实现,只要在分配内存和释放内存时分别做好记录,程序结束时对比分配内存和释放内存的记录就可以确定是不是有内存泄漏。在vs中启用内存检测的方法如下:

STEP1,在程序中包括以下语句:(#include 语句必须采用下文所示顺序。如果更改了顺序,所使用的函数可能无法正常工作。)

#define _CRTDBG_MAP_ALLOC

#include

#include

通过包括 crtdbg.h,将malloc和free函数映射到它们的调试版本,即_malloc_dbg和_free_dbg,这两个函数将跟踪内存分配和释放。此映射只在调试版本(在其中定义了_DEBUG)中发生。 发布版本使用普通的malloc和free函数。

#define 语句将 CRT 堆函数的基版本映射到对应的“Debug”版本。 并非绝对需要该语句;但如果没有该语句,内存泄漏转储包含的有用信息将较少。

STEP2, 在添加了上述语句之后,可以通过在程序中包括以下语句(通常应恰好放在程序退出位置之前)来转储内存泄漏信息:

_CrtDumpMemoryLeaks();

此时,完整的代码如下:

#define_CRTDBG_MAP_ALLOC

#include

#include

 

#include

using namespace std;

 

void GetMemory(char*p,int num)

{

    p = (char*)malloc(sizeof(char)* num);

}

 

int main(int argc,char** argv)

{

    char *str= NULL;

    GetMemory(str,100);

    cout<<"Memory leak test!"<<endl;

    _CrtDumpMemoryLeaks();

    return 0;

}

当在调试器下运行程序时,_CrtDumpMemoryLeaks将在“输出”窗口中显示内存泄漏信息。 内存泄漏信息如下所示:


如果没有使用 #define_CRTDBG_MAP_ALLOC 语句,内存泄漏转储将如下所示:


内存泄露报告说明:

1) 大括号内的127:内存分配编号

2)大括号后的normal block:块类型(普通、客户端或 CRT)

“普通块”是由程序分配的普通内存

“客户端块”是由 MFC 程序用于需要析构函数的对象的特殊类型内存块。 MFC new 操作根据正在创建的对象的需要创建普通块或客户端块。

“CRT 块”是由 CRT 库为自己使用而分配的内存块。 CRT 库处理这些块的释放,因此您不大可能在内存泄漏报告中看到这些块,除非出现严重错误(例如 CRT 库损坏)。

从不会在内存泄漏信息中看到下面两种块类型:

“可用块”是已释放的内存块。

“忽略块”是您已特别标记的块,因而不出现在内存泄漏报告中。

3) CD CD:十六进制形式的内存位置。

4) 定义了 _CRTDBG_MAP_ALLOC 时,还会显示在其中分配泄漏的内存的文件。文件名后括号中的数字(本示例中为 10)是该文件中的行号。

注意:如果程序总是在同一位置退出,调用 _CrtDumpMemoryLeaks 将非常容易。如果程序从多个位置退出,则无需在每个可能退出的位置放置对 _CrtDumpMemoryLeaks 的调用,而可以在程序开始处包含以下调用:

_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF |_CRTDBG_LEAK_CHECK_DF );

该语句在程序退出时自动调用 _CrtDumpMemoryLeaks。必须同时设置 _CRTDBG_ALLOC_MEM_DF 和_CRTDBG_LEAK_CHECK_DF 两个位域,如前面所示。

 

3.1.2  定位具体的内存泄漏地方

通过上面的方法,我们几乎可以定位到是哪个地方调用内存分配函数malloc和new等,如上例中的GetMemory函数中,即第10行!但是不能定位到,在哪个地方调用GetMemory()导致的内存泄漏,而且在大型项目中可能有很多处调用GetMemory。如何要定位到在哪个地方调用GetMemory导致的内存泄漏?

定位内存泄漏的另一种技术涉及在关键点对应用程序的内存状态拍快照。 CRT 库提供一种结构类型_CrtMemState,您可用它存储内存状态的快照:

_CrtMemState s1, s2, s3;

若要在给定点对内存状态拍快照,请向_CrtMemCheckpoint函数传递_CrtMemState 结构。 该函数用当前内存状态的快照填充此结构:

_CrtMemCheckpoint( &s1 );

通过向_CrtMemDumpStatistics 函数传递_CrtMemState 结构,可以在任意点转储该结构的内容:

_CrtMemDumpStatistics( &s1 );

若要确定代码中某一部分是否发生了内存泄漏,可以在该部分之前和之后对内存状态拍快照,然后使用_CrtMemDifference比较这两个状态:

_CrtMemCheckpoint( &s1 );

// memoryallocations take place here

_CrtMemCheckpoint( &s2 );

if ( _CrtMemDifference( &s3, &s1, &s2) )

   _CrtMemDumpStatistics(&s3 );

顾名思义,_CrtMemDifference 比较两个内存状态(s1 和 s2),生成这两个状态之间差异的结果(s3)。 在程序的开始和结尾放置_CrtMemCheckpoint 调用,并使用_CrtMemDifference 比较结果,是检查内存泄漏的另一种方法。如上面的例子程序我们可以这样来定位确切的调用GetMemory的地方:

#define_CRTDBG_MAP_ALLOC

#include

#include

 

#include

using namespace std;

 

_CrtMemStates1, s2, s3;

 

void GetMemory(char*p,int num)

{

    p = (char*)malloc(sizeof(char)* num);

}

 

int main(int argc,char** argv)

{

    _CrtMemCheckpoint( &s1 );

    char *str= NULL;

    GetMemory(str,100);

    _CrtMemCheckpoint( &s2 );

    if ( _CrtMemDifference( &s3, &s1, &s2) )

        _CrtMemDumpStatistics(&s3 );

    cout<<"Memory leak test!"<<endl;

    _CrtDumpMemoryLeaks();

    return 0;

}

调试时,程序输出如下结果:

C++内存泄露和内存管理_第4张图片

这说明在s1和s2之间存在内存泄漏!!!如果GetMemory不是在s1和s2之间调用,那么就不会有信息输出。

{150}表示申请的第150块申请的内存空间;

 

3.1.3  C++显示内存泄露所在的文件以及行

以上这些方法适用于使用标准CRT malloc 函数分配的内存。不过,如果程序使用C++new运算符分配内存,则需要重新定义new才能在内存泄漏报告中看到文件和行号。您可以利用如下所示的代码块实现:

#define _CRTDBG_MAP_ALLOC

#include

#include

#include

using namespace std;

 

#ifdef _DEBUG

#ifndef DBG_NEW

#define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ ,__LINE__)

#define new DBG_NEW

#endif

#endif  // _DEBUG

 

void GetMemory(char *p, int num)

{

    p = new char[10];

}

 

int main(int argc,char** argv)

{

    char *str = NULL;

    GetMemory(str, 100);

   

    cout<<"Memory leaktest!"<<endl;

    _CrtDumpMemoryLeaks();

    return 0;

}


 

3.1.4 显示内存泄露处的堆栈

在申请的堆区序号为lBreakAlloc处设置一个断点  

(函数详细信息参考:http://technet.microsoft.com/zh-cn/library/aa246759

long _CrtSetBreakAlloc( long lBreakAlloc );  

此函数在指定的申请堆区空间次序处(即lBreakAlloc)设置断点;

很喜欢这个函数,这个函数结合3.1.4中的{127},比如使用方法:

_CrtSetBreakAlloc(127); //则在127次申请堆空间时候设置断点  

这样就可以看到函数调用栈,从而帮助我们更加精确的定位程序泄露的位置(调用栈可是个好玩意)。

个人感觉这种方式虽然要手动的修改代码,但因为能够在程序运行的时候查看调用栈,这就意味着能够调试程序。展示结果如下图所示(自动在第127次申请堆空间处中断):

首先增加_CrtSetBreakAlloc(127);,然后再watch窗口增加_crtBreakAlloc

C++内存泄露和内存管理_第5张图片

继续调试,就会弹出如下窗口,这个时候就可以看栈调用了:

 C++内存泄露和内存管理_第6张图片

4 内存泄露检测工具

Windows平台:常用的有Purify,BoundsCheaker、Deleaker、VisualLeak Detector(VLD), Windows NT的Performance Monitor(无法标示出发生问题的代码,但是它能检测出隐式的内存泄漏的存在)等

Linux平台:Valgrind memcheck

使用都很简单

例如VLD,安装VLD后在工程中设置好VLD的头文件包含路径和lib库路径,然后类似这样:

#ifdef _DEBUG  

#include "vld.h"

#endif

 

int main(intargc, char* argv[])

{

    int* pTest = newint;

    //if(NULL != pTest)

    //{

    //  delete ptest;

    //}

    return0;

}

F5运行完毕后,VS输出窗口提示如下信息:

 C++内存泄露和内存管理_第7张图片

 

5 避免内存泄露的方法:

内存泄露检测是事后补救措施了,更重要的是养成好的编码习惯,代码第一次就写对。以下是一些可以参考的避免内存泄露的方法。

 

5.1 对象计数

方法:在对象构造时计数++,析构时--,每隔一段时间打印对象的数量

优点:没有性能开销,几乎不占用额外内存。定位结果精确。

缺点:侵入式方法,需修改现有代码,而且对于第三方库、STL容器、脚本泄漏等因无法修改代码而无法定位。

 

5.2 重载new和delete

方法:重载new/delete,记录分配点(甚至是调用堆栈),定期打印。

优点:没有看出

缺点:侵入式方法,需将头文件加入到大量源文件的头部,以确保重载的宏能够覆盖所有的new/delete。记录分配点需要加锁(如果你的程序是多线程),而且记录分配要占用大量内存(也是占用的程序内存)。

 

5.3 使用智能指针

auto_ptr、unique_ptr、shared_ptr(BOOST库提供)

 

5.4 良好的编码习惯

尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。

使用了内存分配的函数,要记得要使用其想用的函数释放掉,一旦使用完毕。

1) Heap memory

成对使用:

分配函数

释放函数

malloc/realloc

free

new/new[]

delete/delete[]

GlobalAlloc

GlobalFree

要特别注意数组对象的内存泄漏

int *ptr =new int[100];其删除形式为:delete []ptr;

2) Resource Leak

对于系统资源使用之前要仔细看起使用方法,防止错误使用或者忘记释放掉系统资源。

对于资源,也可使用RAII,RAII(Resource acquisition is initialization)资源获取即初始化,它是一项很简单的技术,利用C++对象生命周期的概念来控制程序的资源,例如内存,文件句柄,网络连接以及审计追踪(audit trail)等.RAII的基本技术原理很简单.若希望保持对某个重要资源的跟踪,那么创建一个对象,并将资源的生命周期和对象的生命周期相关联。利用C++的对象管理管理资源. 

 

6 内存溢出

最后再谈谈内存溢出。内存越界、栈溢出、堆栈溢出、缓冲区溢出都是内存溢出。

 

6.1 内存溢出

是程序要求分配的内存超出了系统的预分配,导致其它内存区域被改写。

例如在分配数组时为其分配的长度是128,但往其中装入超过128个数据时,由于C语言不会对数组操作进行越界检查,就会造成内存溢出错误

 

6.2 堆栈溢出

操作系统所使用的缓冲区 又被称为"堆栈". 在各个操作进程之间,指令会被临时储存在"堆栈"当中,"堆栈"也会出现缓冲区溢出。

堆栈溢出就是缓冲区溢出的一种。就是不顾堆栈中数据块大小,向该数据块写入了过多的数据,导致数据越界,结果覆盖了老的堆栈数据。 

例如程序一: 

#include "stdio.h"

 

int main ( )

{

    printf("Please type your name: ");

char name[8] = {0};

    gets(name);

    printf("Hello, %s!",name);

    return0;

}

编译并且执行,我们输入ipxodi,就会输出Hello,ipxodi!。程序运行中,堆栈是怎么操作的呢? 

在main函数开始运行的时候,堆栈里面将被依次放入返回地址,EBP。 

我们用vs2008 来获得汇编语言输出,可以看到char name[8] = {0};对应如下语句(黄底色): 

    printf("Please type your name: ");

003D1028 mov         esi,esp

003D102A push        offset string"Please type your name: " (3D3138h)

003D102F call        dword ptr[__imp__printf (3D30D8h)]

003D1035 add         esp,4

003D1038 cmp         esi,esp

003D103A call        _RTC_CheckEsp(3D10E0h)

    char name[8] = {0};

003D103F mov         byte ptr[ebp-10h],0

003D1043  xor         eax,eax

003D1045 mov         dword ptr[ebp-0Fh],eax

003D1048 mov         word ptr[ebp-0Bh],ax

003D104C mov         byte ptr[ebp-9],al

    gets(name);

003D104F mov         esi,esp

003D1051 lea         eax,[ebp-10h]

003D1054 push        eax 

003D1055 call        dword ptr[__imp__gets (3D30E0h)]

003D105B add         esp,4

003D105E cmp         esi,esp

003D1060 call        _RTC_CheckEsp(3D10E0h)

首先ebp减8(byte ptr [ebp-10h],0),就是堆栈向上增长8个字节。

现在我们再执行一次,输入ipxodiAAAAAAAAAAAAAAA,执行完gets(name)之后,由于我们输入的name字符串太长,name数组容纳不下,只好向内存顶部继续写‘A’。由于堆栈的生长方向与内存的生长方向相反,这些‘A’覆盖了堆栈的老的元素。在main返回的时候,就会把‘AAAA’的ASCII码:0x41414141作为返回地址,CPU会试图执行0x41414141处的指令,结果出现错误。这就是一次堆栈溢出。 

 

6.3 栈溢出

函数中声明的变量过大,超出了函数栈的大小限制,导致程序出错

当程序中一旦调用到fun()函数就会异常

C++内存泄露和内存管理_第8张图片

栈的容量一般由编译器指定,很多编译器也留了选项供程序员设定,例如vs2008中,

Project->工程名 Properties->Configuration Properties->Linker->System->***
也可以在代码中:
#pragma comment(linker, "/STACK:5000000 ")//用该语句设置/STACK:后面数字为容量

 

6.4 内存越界

简单的说,你向系统申请了一块内存,在使用这块内存的时候,超出了你申请的范围。

什么原因会造成内存越界使用呢?有以下几种情况,可供参考:

例1:

char buf[32]= {0};

for(int i=0; i<n; i++)// n > 32

{

    buf[i]= 'x';

}

.... 

例2:

char buf[32]= {0};

string str = "this is a test sting !!!!";

sprintf(buf,"this is a test buf!string:%s", str.c_str());//out of buffer space

....   

例3:

string str = "this is a test string!!!!";

char buf[16]= {0};

strcpy(buf, str.c_str());//out of buffer space

类似的还存在隐患的函数还有:strcat,vsprintf等
同样,memcpy, memset, memmove等一些内存操作函数在使用时也一定要注意。当这样的代码一旦运行,错误就在所难免,会带来的后果也是不确定的,通常可能会造成如下后果:
1)破坏了堆中的内存分配信息数据,特别是动态分配的内存块的内存信息数据,因为操作系统在分配和释放内存块时需要访问该数据,一旦该数据被破坏,以下的几种情况都可能会出现。 

*** glibc detected*** free(): invalid pointer:

*** glibc detected*** malloc(): memory corruption:

*** glibc detected*** double free or corruption(out):0x00000000005c18a0 ***

*** glibc detected*** corrupted double-linked list:0x00000000005ab150 ***  

2)破坏了程序自己的其他对象的内存空间,这种破坏会影响程序执行的不正确性,当然也会诱发coredump,如破坏了指针数据。
3)破坏了空闲内存块,很幸运,这样不会产生什么问题,但谁知道什么时候不幸会降临呢?
通常,代码错误被激发也是偶然的,也就是说之前你的程序一直正常,可能由于你为类增加了两个成员变量,或者改变了某一部分代码,coredump就频繁发生,而你增加的代码绝不会有任何问题,这时你就应该考虑是否是某些内存被破坏了。
排查的原则,首先是保证能重现错误,根据错误估计可能的环节,逐步裁减代码,缩小排查空间。
检查所有的内存操作函数,检查内存越界的可能。常用的内存操作函数:

sprintf snprintf

vsprintf vsnprintf

strcpy strncpystrcat

memcpy memmovememset bcopy

如果有用到自己编写的动态库的情况,要确保动态库的编译与程序编译的环境一致。

 

6.5 缓冲区溢出:

缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖在合法数据上,理想的情况是程序检查数据长度并不允许输入超过缓冲区长度的字符,但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患.

 

6.6 常见的内存溢出问题:

1)内存分配未成功,却使用了它。

常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p 是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc 或new 来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。

2)内存分配虽然成功,但是尚未初始化就引用它。

3)内存分配成功并且已经初始化,但操作越过了内存的边界。

例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for 循环语句中,循环次数很容易搞错,导致数组操作越界。

4)使用free 或delete 释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

5)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。

6)不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

 

你可能感兴趣的:(技术总结,内存泄露,内存溢出,C++,内存)