C语言内存泄露严重应对方式策略探讨.docx

上传人:夺命阿水 文档编号:1203912 上传时间:2024-04-01 格式:DOCX 页数:10 大小:34.69KB
返回 下载 相关 举报
C语言内存泄露严重应对方式策略探讨.docx_第1页
第1页 / 共10页
C语言内存泄露严重应对方式策略探讨.docx_第2页
第2页 / 共10页
C语言内存泄露严重应对方式策略探讨.docx_第3页
第3页 / 共10页
C语言内存泄露严重应对方式策略探讨.docx_第4页
第4页 / 共10页
C语言内存泄露严重应对方式策略探讨.docx_第5页
第5页 / 共10页
点击查看更多>>
资源描述

《C语言内存泄露严重应对方式策略探讨.docx》由会员分享,可在线阅读,更多相关《C语言内存泄露严重应对方式策略探讨.docx(10页珍藏版)》请在课桌文档上搜索。

1、1.前言最近部门不同产品接连出现内存泄漏导致的网上问题,具体表现为单板在现网运行数月以后,因为内存耗尽而导致单板更位现象。一方面,内存泄漏问题属于低级错误,此类问题遗漏到现网,影响很坏;另一方面,由于内存泄漏问题很可能导致单板运行固定时间以后就复位,只能通过批量升级才能解决,实际影响也很恶劣。同时,接连出现此类问题,尤其是其中一例问题还是我们老员工修改引入,说明我们不少员工对内存泄漏问题认识还是不够深刻的。本文通过介绍内存泄漏问题原理及检视方法,希望后续能够从编码检视环节就杜绝此类问题发生。说明:预防内存泄漏问题有多种方法,如加强代码检视、工具检测和内存测试等,本文聚集于开发人员能力提升方面。

2、一、什么是内存泄漏?在C语言中,我们可以使用malloc、CanOc、realloc等函数来动态地申请内存空间,这些内存空间是从堆(heap)中分配的,与程序的生命周期无关,只有当我们显式地调用free函数来释放这些内存空间时,它们才会被回收。这样,我们就可以根据需要动态地调整内存的大小和数量,提高内存的利用率和程序的灵活性。但是,这种动态内存分配的方式也带来了一些风险,就是如果我们在使用完动态分配的内存空间后,忘记或者无法释放它们,那么这些内存空间就会一直占用着系统的内存资源,无法被其他程序使用,这就是内存泄漏(memoryleak)。简单的内存泄漏示例代码:l#include23voida

3、llocateMemory()4int*ptr=(int*)malloc(sizeof(int);5if(ptr=NULL)6exit(l);78*ptr=10;9注意:这里缺少了free(ptr)的调用,导致内存泄漏10)1112intmain()13for(inti=0;i10000;i+)14allocateMemory();16retum0;17)上面的例子中,alIocateMemory函数使用malloc分配了一块内存,但之后并没有调用free释放这块内存。因此,每次调用allocateMemory函数时,都会有一块内存无法被释放,这就是内存泄漏。二、内存泄漏的常见原因和表现1.内

4、存泄漏的常见原因(1)忘记释放内存当使用malloc、calloc或realloc等函数分配内存后,必须在使用完内存后使用free来释放它。如果忘记释放,就会导致内存泄漏。示例代码:l#include23VoidforgetToFreeO4int*ptr=(int*)malloc(sizeof(int);5if(ptr!=NULL)6*ptr=10;7忘记调用free(ptr)89)1011intmain(intargc,char*argv)12for(inti=0;i10000;i+)13forgetToFree();1415retum0;16(2)重复释放尝试多次释放同一块内存是非法的,可

5、能导致程序崩溃。示例代码:l#include23voiddoubleFree()(4int*ptr=(int*)malloc(sizeof(int);5if(ptr!=NULL)6free(ptr);7重第释放同一块内存8free(ptr);9)10)11)12intmain(intargc,char*argv)(13doubleFree();14retum0;(3)内存泄漏在函数中在函数内部分配的内存,如果没有被返回给调用者,调用者就无法释放它.示例代码:l#include23voidallocateInFunction()4int*ptr=(int*)malloc(sizeof(int);

6、5if(ptr!=NULL)6Ptr没有返回给调用者,导致内存泄漏7*ptr=10;89)1011intmain(intargc,char*argv)12allocatelnFunction();13无法释放allocatelnFunction中分配的内存14retum0;(4)指针丢失如果丢失了指向已分配内存的指针,那么这块内存就无法被释放。示例代码:l#include2int*someOtherFunction()3假设这个函数返回一个新的指针4int*newPtr=(int*)malloc(sizeof(int);5returnnewPtr;6)78voidloseReference()

7、9int*ptr=(int*)malloc(sizeof(int);10if(ptr!=NULL)1l*ptr=10;12假设Ptr被用于某个全局数据结构或传递给其他函数13/.14然后ptr的引用丢失了,例如它指向的内存被另一个指针覆盖15假设这个函数返回一个新的指针16int*newPtr=someOtherFunction();17现在Ptr指向了新的内存,原先的内存泄漏了18ptr=newPtr;19或者ptr可能被置为NULL,或者超出了其作用域20)21)2223intmain(intargc,char*argv)j24调用函数,Ptr曾经指向一块内存251oseReference

8、();26但由于在某处丢失了Ptr的引用,这块内存无法被释放,导致内存泄漏27retum0;28)2 .内存泄漏的表现(1)程序性能下降随着程序运行时间的增长,内存占用逐渐增加,导致程序运行缓慢。(2)程序崩溃当可用内存耗尽时,程序可能会因为无法分配更多内存而崩溃。(3)不可预测的行为内存泄漏可能导致程序出现各种不可预测的行为,如数据损坏、访问违规等。3 .内存泄漏问题原理3.1 堆内存在C代码中的存储方式内存泄漏问题只有在使用堆内存的时候才会出现,栈内存不存在内存泄漏问题,因为栈内存会自动分配和释放。C代码中堆内存的申请函数是malloc,常见的内存申请代码如下:Char*info=NULL

9、;/*转换后的字符串*/info=(char*)malloc(NB_MEM_SPD_INFO_MAX_SIZE);if(NULL=info)(void)tdm-error(mallocerror!n);returnNB_SA_ERR_HPl_OUT_OF_MEMORY;)由于malloc函数返回的实际上是一个内存地址,所以保存堆内存的变量一定是一个指针(除非代码编写极其不规范)。再重复一遍,保存堆内存的变量一定是一个指针,这对本文主旨的理解很重要。当然,这个指针可以是单指针,也可以是多重指针。malloc函数有很多变种或封装,如g.mallocxg_mallocO、VOS_Malloc等,这些

10、函数最终都会调用malloc函数。3.2 堆内存的获取方法看到本小节标题,可能有些同学有疑惑,上一小节中的malloc函数,不就是堆内存的获取方法吗?的确是,通过malloc函数申请是最直接的获取方法,如果只知道这种堆内存获取方法,就容易掉到坑里了。一般的来讲,堆内存有如下两种获取方法:方法一:将函数返回值直接赋给指针,一般表现形式如下:char*local_pointer_xx=NULL;local_pointer_xx=(char*)function_xx(para_xx,-);该类涉及到内存申请的函数,返回值一般都指针类型,例如:GSList*g_slist_append(GSList*

11、list,gpointerdata);方法二:将指针地址作为函数返回参数,通过返回参数保存堆内存地址,一般表现形式如下:intret;Char*loCaLPOintejxx=NULL;/*转换后的字符串*/ret=(char*)function_xx(.,&local_pointer_xx,.);该类涉及到内存申请的函数,一般都有一个入参是双重指针,例如:_STDIOJNLINEJO_ssize_t;getline(char*_Iineptr,size_t*_n,FILE*_stream);前面说通过malloc申请内存,就属于方法一的一个具体表现形式。其实这两类方法的本质是一样的,都是函数内

12、部间接申请了内存,只是传递内存的方法不一样,方法一通过返回值传递内存指针,方法二通过参数传递内存指针3.3 内存泄漏三要素最常见的内存泄漏问题,包含以下三个要素:要素一:函数内有局部指针变量定义;要素二:对该局部指针有通过上一小节中“两种堆内存获取方法”之一获取内存;要素三:在函数返回前(含正常分支和异常分支)未释放该内存,也未保存到其它全局变量或返回给上一级函数。3.4 内存释放误区稍微使用过C语言编写代码的人,都应该知道堆内存申请之后是需要释放的。但为何还这么容易出现内存泄漏问题呢?一方面,是开发人员经验不足、意识不到位或一时疏忽导致;另一方面,是内存释放误区导致。很多开发人员,认为要释放

13、的内存应该局限于以下两种:1)直接使用内存申请函数申请出来的内存,如malloc、g_ma11oc等;2)该开发人员熟悉的接口中,存在内存申请的情况,如iBMC的兄弟,都应该知道调用如下接口需要释放IiSt指向的内存:dfl_get_objectJist(constchar*class_name,GSList*list);按照以上思维编写代码,一旦遇到不熟悉的接口中需要释放内存的问题,就完全没有释放内存的意识,内存泄漏问题就自然产生了。4 .内存泄漏问题检视方法检视内存泄漏问题,关键还是要养成良好的编码检视习惯。与内存泄漏三要素对应,需要做到如下三点:1)在函数中看到有局部指针,就要警惕内存泄

14、漏问题,养成进一步排查的习惯2)分析对局部指针的赋值操作,是否属于前面所说的“两种堆内存获取方法”之一,如果是,就要分析函数返回的指针到底指向啥?是全局数据、静态数据还是堆内存?对于不熟悉的接口,要找到对应的接口文档或源代码分析;又或者看看代码中其它地方对该接口的引用,是否进行了内存释放;3)如果确认对局部指针存在内存申请操作,就需要分析该内存的去向,是会被保存在全局变量吗?又或者会被作为函数返回值吗?如果都不是,就需要排查函数所有有“return”的地方,保证内存被正确释放。三、如何避免内存泄漏?及时释放内存确保在使用完内存后,使用free函数及时释放内存。避免重复释放:在释放内存后,将指针

15、设置为NULL,以防止重复释放。检查内存分配:在分配内存后,检查指针是否为NULL如果malloc或CaikK函数返回NULL,表示内存分配失败,此时不应继续使用该指针。编写健壮的代码:避免在异常情况下(如函数提前返回或遇到错误)忘记释放内存。学习和遵守最佳实践:了解常见的内存泄漏模式,并遵循编写高效、健壮代码的最佳实践。使用内存检测工具:使用如Valgrind等内存检测工具来检测内存泄漏和其他内存问题。嵌入式C语言中的内存泄漏问题只有在堆内存里面才会发生内存泄漏的问题,在栈内存中不会发生内存泄漏。因为栈内存在自动分配空间之后,还会自动释放空间。什么是堆内存?存储方式是什么样的呢?首先我们先来

16、介绍一下堆内存在C代码中的存储方式。C代码中动态申请堆内存的申请函数是malkc,常见的内存代码如下图所示:1 includstdo.h2 intmain()3 14 char*in=NULL;in=(chGr*)malloc(HELLO_WORLD_I_AM_XIAO_CHENG);6 if(NULL=in)7 (void)t_error(mallocerror!n);I_AM-XlAoCHENG_MEMORY;10H因为malloc函数返回值是一个内存地址,所以保存堆内存的变量一定得是一个指针,当然这个变量可以是一个单指针,也可以是一个多重指针。如何获取堆内存?对于堆内存的获取方法,我们可

17、以有两种方法,第一种是用返回值传递内存指针,第二种方法是通过参数传递给内存指针。上面我们用到的malloc申请内存,就是属于方法一的一种具体表现形式,是直接把返回值传递给内存指针。方法一:把函数返回值直接赋值给指针,一般表现形式如下:1char*name_phone_xx=NULL;2name_phone_xx=(char*)function-x(para-x,.);方法二:将指针地址作为函数返回参数,通过返回参数保存堆内存地址,一般表现形式如下:1 intvim;2 char*name_phone_xx=NULL;vim(char*)function_xx(.,name-phone-xx,.

18、);总结:这两类方法的本质是一样的,都是函数内存间接申请了内存,但是只有传递内存的方法不一样,方法一是通过返回值传递内存指针,方法二是通过参数传递内存指针。内存泄漏的三个原因当我们的代码出现内存泄漏的时候,一般都会包含以下几个原因:1、函数内有局部指针变量定义2、对该局部指针有获取内存的操作3、在函数返回前没有释放该内存,也未保存到其他全局变量或返回上一级函数如何检查内存泄漏为了避免检查内存泄漏,我们还是要养成良好的编码习惯。当我们要进行检查内存泄漏问题的时候,一般要做到以下三点:1、当我们在函数中看到有局部指针的时候,一定要仔细检查是否有存泄漏的问题发生,养成仔细检查的习惯。2、如果有局部变

19、量,并且有对局部变量赋值的操作,要检查函数的返回的指针到底是指向什么?是全局变量、静态数据还是堆内存?如果代码中有不熟悉的接口,要找到对应接口文档或源代码分析,保证不要出现不必要的错误。3、如果函数中有对局部指针有内存申请的操作,那么要检查被保存的是全局变量吗?会被作为函数返回值吗?如果都不是的话,那要排查函数所有的“return”的地方,要保证内存被正确释放,不占用内存。避免在堆上分配众所周知,大部分的内存泄漏都是因为在堆上分配引起的,如果我们不在堆上进行分配,就不会存在内存泄漏了(这不废话嘛),我们可以根据具体的使用场景,如果对象可以在栈上进行分配,就在栈上进行分配,一方面栈的效率远高于堆

20、,另一方面,还能避免内存泄漏,我们何乐而不为呢。手动释放对于malloc函数分配的内存,在结束使用的时候,使用free函数进行释放对于new操作符创建的对象,切记使用delete来进行释放对于new。创建的对象,使用delete。来进行释放(使用free或者delete均会造成内存泄漏)避免使用裸指针尽可能避免使用裸指针,除非所调用的Iib库或者合作部门的接口是裸指针。intfun(int*ptr)fun是一个接口或Iib函数/dosthreturn;intmain()inta=1000;int*ptr=fea;/.fun(ptr);returnO;)在上面的fun函数中,有一个参数ptr,为

21、int*,我们需要根据上下文来分析这个指针是否需要释放,这是一种很不好的设计使用STL中或者自己实现对象在C+中,提供了相对完善且可靠的STL供我们使用,所以能用STL的尽可能的避免使用C中的编程方式,比如:使用std:string替代Char*,string类自己会进行内存管理,而且优化的相当不错使用std:vector或者std:array来替代传统的数组其它适合使用场景的对象智能指针自C+11开始,STL中引入了智能指针(SmartPointer)来动态管理资源,针对使用场景的不同,提供了以下三种智能指针。unique_ptrunique_ptr是限制最严格的一种智能指针,用来替代之前的

22、auto.ptr,独享被管理对象指针所有权。当UniqUe_ptr对象被销毁时,会在其析构函数内删除关联的原始指针。unique_ptr对象分为以下两类:UniqUe_Ptr该类型的对象关联了单个Type类型的指针std:unique_ptrpl(newType);/c+11autop1=std:make_unique();/c+14UniqUe_ptr该类型的对象关联了多个Type类型指针,即一个对象数组std:unique_ptrp2(newTypen();/c+lIautop2=std:make_unique(n);/c+14不可用被复制unique-ptra(newint(O);Uni

23、qUe_ptrb=a;/编译错误UniqUe_ptrb=std:move(a);可以通过move语义进行所有权转移根据使用场景,可以使用std:UniqUe_ptr来避免内存泄漏,如下:voidfun()unique_ptra(newint(O);/usea)在上述fun函数结束的时候,会自动调用a的析构函数,从而释放其关联的指针。shared_ptr与UniqUe_ptr不同的是,unique_ptr是独占管理权,而shared_ptr则是共享管理权,即多个Shared_ptr可以共用同一块关联对象,其内部采用的是引用计数,在拷贝的时候,引用计数+1,而在某个对象退出作用域或者释放的时候,引

24、用计数当引用计数为0的时候,会自动释放其管理的对象。voidfun()std:shared_ptra;/a是一个空对象std:shared_ptrb=std:make_shared();分酉己资源a=t此时引用计数为2std:Share(LPtrc=a;此时引用计数为3C退出作用域,此时引用计数为2b退出作用域,此时引用计数为la退出作用域,引用计数为0,释放对象weak_ptrweak_ptr的出现,主要是为了解决Share(LPtr的循环引用,其主要是与Share(LPtr一起来私用。和Shared_plr不同的地方在于,其并不会拥有资源,也就是说不能访问对象所提供的成员函数,不过,可以通

25、过Weak_ptr.lock()来产生一个拥有访问权限的Shared_ptr。std:weak_ptra;std:shared_ptrb=std:make_shared();a=bb所对应的资源释放RAIIRAIl是ReSOUrCeACqUiSitioniSInitiaIiZation(资源获取即初始化)的缩写,是C+语言的一种管理资源,避免泄漏的用法。利用的就是C+构造的对象最终会被销毁的原则。利用C+对象生命周期的概念来控制程序的资源,比如内存,文件句柄,网络连接等。RAIl的做法是使用一个对象,在其构造时获取对应的资源,在对象生命周期内控制对资源的访问,使之始终保持有效,最后在对象析构的

26、时候,释放构造时获取的资源。简单地说,就是把资源的使用限制在对象的生命周期之中,自动释放。举个简单的例子,通常在多线程编程的时候,都会用到std:mutex,如下代码:std:mutexmutex_;voidfun()mutex_.lock();if(.)mutex_.unlock();return;mutex-.unlock()在上述代码中,如果if分支多的话,每个if分支里面都要释放锁,如果一不小心忘记释放,那么就会造成故障,为了解决这个问题,我们使用RAIl技术,代码如下:std:mutexmutex_;voidfun()std:lock_guardguard(mutex_);if(.)

27、return;)在guard出了fun作用域的时候,会自动调用mutexOCkO进行释放,避免了很多不必要的问题。定位在发现程序存在内存泄漏后,往往需要定位泄漏点,而定位这一步往往是最困难的,所以经常为了定位泄漏点,采取各种各样的方案,甭管方案优雅与否,毕竟管他白猫黑猫,抓住老鼠才是好猫,所以在本节,简单说下笔者这么多年定位泄漏点的方案,有些比较邪门歪道,您就随便看看就行。日志这种方案的核心思想,就是在每次分配内存的时候,打印指针地址,在释放内存的时候,打印内存地址,这样在程序结束的时候,通过分配和释放的差,如果分配的条数大于释放的条数,那么基本就能确定程序存在内存泄漏,然后根据日志进行详细分

28、析和定位。char3fcfun()char*p=(char*)malloc(20);Printfd%s,%d,addressis:%p,FlLELlNE_,p);/Zdosthreturnp;)intmain()fun();retumO;)统计统计方案可以理解为日志方案的一种特殊实现,其主要原理是在分配的时候,统计分配次数,在释放的时候,则是统计释放的次数,这样在程序结束前判断这俩值是否一致,就能判断出是否存在内存泄漏。此方法可帮助跟踪已分配内存的状态。为了实现这个方案,需要创建三个自定义函数,一个用于内存分配,第二个用于内存释放,最后一个用于检查内存泄漏。代码如下:Staticunsigne

29、dintallocated=O;Staticunsignedintdeallocated=O;void*Memory_Allocate(size_tsize)void*ptr=NULL;ptr=malloc(size);if(NULL!=ptr)+allocated;else/Logerror)returnptr;)voidMemory_Deallocate(void*ptr)if(pvHandle!=NULL)free(ptr);+deallocated;)intCheck_Memory_Leak(void)intret=O;if(allocated!=deallocated)/Logerr

30、orret=MEMoRY_LEAK;)else(ret=0K;returnret;)工具在Linux上比较常用的内存泄漏检测工具是valgrind,所以咱们就以valgrind为工具,进行检测。我们首先看一段代码:#includevoidfunc(void)(char*buff=(char*)malloc(10);)intmain(void)func();产生内存泄漏return;)通过gcc-gleak.c-oleak命令进行编译执行valgrind-leak-check=full.leak在上述的命令执行后,会输出如下:=9652=Memcheck,amemoryerrordetector

31、=9652=Copyright(C)2002-2017,andGNUGPL,d,byJulianSewardetal.=9652=UsingValgrind-3.15.0andLibVEX;rerunwith-hfbrcopyrightinfo=9652=Command:./leak=9652=9652=9652=HEAPSUMMARY:=9652=inuseatexit:1Obytesin1blocks=9652=totalheapusage:1alIocs,Ofrees,1Obytesallocated=9652=9652=1Obytesin1blocksaredefinitelylost

32、inlossrecord1ofl=9652=at0x4C29F73:malloc(vg_replace_malloc.c:309)=9652=by0x40052E:func(leak.c:4)=9652=by0x40053D:main(leak.c:8)=9652=9652=LEAKSUMMARY:=9652=definitelylost:1Obytesin1blocks=9652=indirectlylost:0bytesin0blocks=9652=possiblylost:0bytesin0blocks=9652=stillreachable:0bytesin0blocks=9652=s

33、uppressed:0bytesin0blocks=9652=9652=Forlistsofdetectedandsuppressederrors,rerunwith:-s=9652=ERRORSUMMARY:1errorsfrom1Contexts(SuppressedzOfromO)Valgrind的检测信息将内存泄漏分为如下几类:definitelylost:确定产生内存泄漏indirectlylost:间接产生内存泄漏PoSSiblyloSt:可能存在内存泄漏Stillreachable:即使在程序结束时候,仍然有指针在指向该块内存,常见于全局变量主要上面输出的下面几句:=9652=b

34、y0x40052E:func(leak.c:4)=9652=by0x40053D:main(leak.c:8)提示在main函数(Ieak.c的第8lf)fun函数(Ieak.c的第四行)产生了内存泄漏,通过分析代码,原因定位,问题解决。经验之谈在C/C+开发过程中,内存泄漏是一个非常常见的问题,其影响相对来说远低于CoredUmP等,所以遇到内存泄漏的时候,不用过于着急,大不了重启嘛。在开发过程中遵守下面的规则,基本能90+%避免内存泄漏:良好的编程习惯,只有有malloc/new,就得有free/delete尽可能的使用智能指针,智能指针就是为了解决内存泄漏而产生使用log进行记录也是最重要的一点,谁申请,谁释放对于malloc分配内存,分配失败的时候返回值为NULL,此时程序可以直接退出了,而对于new进行内存分配,其分配失败的时候,是抛出std:bad_alloc,所以为了第一时间发现问题,不要对new异常进行CatCh,毕竟内存都分配失败了,程序也没有运行的必要了。如果我们上线后,发现程序存在内存泄漏,如果不严重的话,可以先暂时不管线上,同时进行排查定位;如果线上泄漏比较严重,那么第一时间根据实际情况来决定是否回滚。在定位问题点的时候,可以采用缩小范围法,着重分析这次新增的代码,这样能够有效缩短问题解决的时间。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 在线阅读 > 生活休闲


备案号:宁ICP备20000045号-1

经营许可证:宁B2-20210002

宁公网安备 64010402000986号