性能文章>C++内存管理中内存泄漏产生的原因和解决办法>

C++内存管理中内存泄漏产生的原因和解决办法转载

2周前
169000

C++内存管理中内存泄露(memory leak)一般指的是程序在申请内存后,无法释放已经申请的内存空间,内存泄露的积累往往会导致内存溢出。

一、内存分配方式

通常内存分配方式有以下三种:

(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

(2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

二、程序内存空间

一个程序将操作系统分配给其运行的内存分为五个区域:

(1)栈区:由编译器自动分配释放,存放为函数运行的局部变量,函数参数,返回数据,返回地址等。操作方式与数据结构中的类似,栈区有以下特点:

  1)由系统自动分配。比如在函数运行中声明一个局部变量int b = 10;,系统自动在栈中为b开辟空间;

  2)只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

(2)堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收;分配方式类似于链表,堆区有以下特点:

  1)需要程序员自己申请,并指明大小,在C中是有malloc函数,在C++中多使用new运算符(从C++角度上说,使用new分配堆空间可以调用类的构造函数,而malloc()函数仅仅是一个函数调用,它不会调用构造函数,它所接受的参数是一个unsigned long类型。同样,delete在释放堆空间之前会调用析构函数,而free函数则不会)。

  2)在操作系统中有一个记录空闲内存地址的表,这是一种链式结构。它记录了有哪些还未使用的内存空间。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

(3)全局数据区:也叫做静态区,存放全局变量,静态数据。程序结束后由系统释放。

(4)文字常量区:可以理解为常量区,常量字符串存放这里。程序结束后由系统释放。“常量”是指它的值是不可变的,同时,虽然常量也是存储在内存的某个地方,但是无法访问常量的地址的。

(5)程序代码区:存放函数体的二进制代码。但是代码段中也分为代码段和数据段。

一个程序内存分配例子:

int a = 0; //全局初始化区
char *p1;  //全局未初始化区
int main() {
    int b; //栈区
    char s[] = /"abc/"; //栈区
    char *p2; //栈区
    char *p3 = /"123456/"; //123456//0在常量区,p3在栈区。
    static int c =0;//全局(静态)初始化区
    p1 = new char[10];
    p2 = new char[20];
    //分配得来得和字节的区域就在堆区。
    strcpy(p1, /"123456/"); //123456//0放在常量区,编译器可能会将它与p3所指向的/"123456/"优化成一个地方。
}


三、内存溢出原因


(1)在类的构造函数和析构函数中没有匹配的调用new和delete函数

  两种情况下会出现这种内存泄露:

  1)在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内存;

  2)在类的构造函数中动态的分配了内存,但是在析构函数中没有释放内存或者没有正确的释放内存。

(2)没有正确地清除嵌套的对象指针

(3)在释放对象数组时在delete中没有使用方括号

  方括号是告诉编译器这个指针指向的是一个对象数组,同时也告诉编译器正确的对象地址值并调用对象的析构函数,如果没有方括号,那么这个指针就被默认为只指向一个对象,对象数组中的其他对象的析构函数就不会被调用,结果造成了内存泄露。如果在方括号中间放了一个比对象数组大小还大的数字,那么编译器就会调用无效对象(内存溢出)的析构函数,会造成堆的奔溃。如果方括号中间的数字值比对象数组的大小小的话,编译器就不能调用足够多个析构函数,结果会造成内存泄露。

  释放单个对象、单个基本数据类型的变量或者是基本数据类型的数组不需要大小参数,释放定义了析构函数的对象数组才需要大小参数。

(4)指向对象的指针数组不等同于对象数组

  对象数组是指:数组中存放的是对象,只需要delete [ ] p,即可调用对象数组中的每个对象的析构函数释放空间

  指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete [ ] p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了。

(5)缺少拷贝构造函数

  两次释放相同的内存是一种错误的做法,同时可能会造成堆的奔溃。

  按值传递会调用(拷贝)构造函数,引用传递不会调用。

  在C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数,会逐个成员拷贝的方式来复制数据成员,如果是以逐个成员拷贝的方式来复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。

  所以,如果一个类里面有指针成员变量,要么必须显示的写拷贝构造函数和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符。

(6)缺少重载赋值运算符

  这种问题跟上述问题类似,也是逐个成员拷贝的方式复制对象,如果这个类的大小是可变的,那么结果就是造成内存泄露.

(7)关于nonmodifying运算符重载的常见错误

  1)返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针(指向被释放的或者访问受限内存的指针);

  2)返回内部静态对象的引用;

  3)返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收。

解决这一类问题的办法是重载运算符函数的返回值不是类型的引用,二应该是类型的返回值,即不是 int&而是int。

(8)没有将基类的析构函数定义为虚函数

  当基类指针指向子类对象时,如果基类的析构函数不是虚函数,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

造成野指针的原因:

  1)指针变量没有被初始化(如果值不定,可以初始化为NULL);

  2)指针被free或者delete后,没有置为NULL, free和delete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为NULL;

  3)指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针;

  4)shared_ptr循环引用。

(9)析构的时候使用void*

  delete掉一个void*类型的指针,导致没有调用到对象的析构函数,析构的所有清理工作都没有去执行从而导致内存的泄露。

(10)构造的时候浅拷贝,释放的时候调用了两侧delete

四、常见解决办法


(1)shared_ptr共享的智能指针:

  shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。

注意事项: 

  1)不要用一个原始指针初始化多个shared_ptr;

  2)不要再函数实参中创建shared_ptr,在调用函数之前先定义以及初始化它;

  3)不要将this指针作为shared_ptr返回出来;

  4)要避免循环引用。

(2)unique_ptr独占的智能指针:

  1)unique_ptr是一个独占的智能指针,他不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另外一个 unique_ptr;

  2)unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再 拥有原来指针的所有权了;

  3)如果希望只有一个智能指针管理资源或管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

(3)weak_ptr弱引用的智能指针:

  弱引用的智能指针weak_ptr是用来监视shared_ptr的,不会使引用计数加一,它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命 周期,更像是shared_ptr的一个助手。 weak_ptr没有重载运算符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中关连的资源是否存在。 weak_ptr还可以用来返回this指针和解决循环引用的问题。

(4)set_new_handler(out_of_memroy);   //注意参数传递的是函数的地址
-----------------------------------

请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
谨防JDK8重复类定义造成的内存泄漏
概述 如今JDK8成了主流,大家都紧锣密鼓地进行着升级,享受着JDK8带来的各种便利,然而有时候升级并没有那么顺利?比如说今天要说的这个问题。我们都知道JDK8在内存模型上最大的改变是,放弃了Perm
JVM菜鸟进阶高手之路二MAT工具相关知识解惑
关于MAT工具相关知识解惑MAT 不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如 Sun, HP, SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM 的
JVM垃圾回收与一次线上内存泄露问题分析和解决过程
本文转载自:花椒技术微信公众号 前言内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。Ja
强如 Disruptor 也发生内存溢出?
前言```OutOfMemoryError ```问题相信很多朋友都遇到过,相对于常见的业务异常(数组越界、空指针等)来说这类问题是很难定位和解决的。本文以最近碰到的一次线上内存溢出的定位、解决问题的
spring boot 引起的 “堆外内存泄漏”
背景组内一个项目最近一直报swap区域使用过高异常,笔者被叫去帮忙查看原因。发现配置的4G堆内内存,但是实际使用的物理内存高达7G,确实有点不正常,JVM参数配置是:```java-XX:Metasp
实战:一次疑似内存泄漏的问题排查
问题背景最近服务器到期等因素,进行了迁移。租了其它的外国厂商,但是由于资费问题,购买了1.5G 内存的服务器(现)。因为原本用惯了4G内存的服务器(原),现在压缩成这样,似乎不太能支持我的使用,囧!现
记录一次Flink作业异常的排查过程
最近2周开始接手apache flink全链路监控数据的作业,包括指标统计,业务规则匹配等逻辑,计算结果实时写入elasticsearch. 昨天遇到生产环境有作业无法正常重启的问题,我负责对这个问题
0
0