天天看点

C++中的内存泄漏

C++中的内存泄漏

参考、转载自大神博客

目录

    • C++中的内存泄漏
      • 1、new(malloc)和delete(free)没有配套使用
      • 2、类的构造函数和析构函数中new等没有配套使用
      • 3、没有正确地清除嵌套的对象指针
      • 4、在释放对象数组时在delete中没有使用方括号
      • 5、指向对象的指针数组不等同于对象数组
      • 6、缺少拷贝构造函数
      • 7、缺少重载赋值运算符
      • 8、关于nonmodifying运算符重载的常见迷思
      • 9、没有将基类的析构函数定义为虚函数
      • 10、补充

1、new(malloc)和delete(free)没有配套使用

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

这种是最简单,最直接的内存泄露,很容易发现

2、类的构造函数和析构函数中new等没有配套使用

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

3、没有正确地清除嵌套的对象指针

如下:

int* a = new int;
  int** b = new int*;
  *b = a;
           

这样的情况下,如果你先把b释放了,a自身的值就没了,导致a原来申请的空间现在没人管了,造成了内存泄漏。

4、在释放对象数组时在delete中没有使用方括号

这个其实有个经典的例子,再说例子之前先说一下基本知识

在使用new,来创建一个指向class的指针的时候,new做的事情有三个:

  1. 分配内存
  2. 指针转型
  3. 调用构造函数

其中第二个这里不讨论,然后再看delete做的事情:

  1. 调用析构函数
  2. 释放内存

接下来看例子:

我们定义拥有三个指针的指针数组,每个指针指向一个class(下图左半部分)

每个class里面包含指针,指向了其他地方(下图右半部分)

C++中的内存泄漏

我们使用 delete [] 的时候,编译器知道,我要释放三个class,

然后依次调用三个析构函数,将右半部分的空间释放了

然后就是释放左半部分的空间。

这是正确的做法。

C++中的内存泄漏

还是刚刚一样结构的指针数组,

此时我们使用 delete ,编译器以为,自己要释放一个class

然后就只调用了第一个class的析构函数(右半部分空框框的空间被释放了)

但是其余两个class并没有调用析构函数(右半部分问号框框的空间没被释放)

然后编译器就把左半部分的空间给释放了

导致那两个class的空间无法控制了,造成了内存泄露

这是错误的做法

理解之后,我感觉这种内存泄漏有种 2 3 结合的意思:

因为嵌套导致了class的new和delete不配套

说这么多的原因是提醒大家不要对 delete[] 造成误解,

用不用delete的方括号,影响的并不是数组本身的空间(左半部分),数组只要调用了delete就会被释放

他影响的主要是数组内容指向的空间(右半部分),可能导致因为没有调用析构函数而造成内存泄露

(这一部分主要参考侯捷的视频讲解,视频讲解地址,这个视频并不只是讲了这些东西,这部分是在视频的42分钟开始讲的,上面两个图也是视频中的截图)

5、指向对象的指针数组不等同于对象数组

这种泄露有点像第四种,

  1. 对象数组:数组的每个元素是对象本身
  2. 指向对象的指针数组:数组的每个元素是指针,每个指针指向的是对象

小窍门: 理解各种相似名字的时候,先不要看他的修饰词,就只看名词本身。

指向对象的指针数组:他就是一个数组,数组的每个元素是指针

数组指针:他就是一个指针,是指向数组的指针

针对对象数组:

释放的时候,直接 delete[] 就可以,刚刚讨论过了

针对指向对象的指针数组:

直接 delete[] 是不行的,因为他释放的是指针的空间,指针指向的空间并没有释放(析构函数没有调用)

应该先遍历数组,依次调用每个元素的析构函数

最后使用 delete[],将数组中的每个指针释放了

6、缺少拷贝构造函数

这部分主要参考侯捷视频,视频链接,是从22分钟开始将这部分的内容

缺少的意思并不是没有,系统有默认的,但是默认的并不能达到所有要求,这时候就必须自己再写一个

这种泄露是因为两次调用了delete,

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

这里,我分了三种情况(主要讨论后面两种情况)

  1. 按值传递(默认、自己写)
  2. 指针传递(默认)
  3. 指针传递(自己写)

1. 按值传递(默认、自己写)

这种情况,拷贝构造函数不管是默认的还是自己写的,都是将值复制一份,然后给class,没什么说的

2. 指针传递(默认)

这种的话,可以解决大部分情况,但是也有不能解决的情况。

假设我们有这一个class a,他内部有个指针,指向了hello,已经分配过了空间

C++中的内存泄漏

这时候,我们new一个class b,让他等于class a,如果没有写新的拷贝构造,他就会调用默认的,

默认的他只一个功能,一个对一个的复制。最终会变成这样

C++中的内存泄漏

两个class里面的指针都指向了hello这个空间

一旦这时候,调用了class a或者class b的析构函数,hello所在的空间会被释放,

这时候,另一个class的指针虽然还是指向那个空间,但是,内容已经没有了,这种指针有一个名字 “悬空指针”

不管之后是使用这个指针,还是调用析构函数释放这个指针,都会引发不可预知的错误。

这就是内存泄露

这种形式的拷贝也有一个名字 “浅拷贝”

3. 指针传递(自己写)

发现了默认拷贝构造函数的问题,我们就需要自己写一个。

过程:

  1. 给class b的指针分配空间
  2. 使用复制函数(strcpy),将class a中指针指向的hello复制给class b一份。

执行之后是这样的

C++中的内存泄漏

这样的就没有内存泄露

这种方式的拷贝叫 “深拷贝”

7、缺少重载赋值运算符

这种情况与 6 相似,是因为默认的赋值运算符,并没有特殊的操作。比如下面这种

C++中的内存泄漏

执行之后,P1原来指向的空间,因为没有调用delete,导致失控

而且,赋值之后,P1和P2指向了同一块空间,这种情况刚刚讨论过,也会导致内存泄漏

8、关于nonmodifying运算符重载的常见迷思

这个我不理解,照抄了

  1. 返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针
  2. 返回内部静态对象的引用。
  3. 返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收

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

9、没有将基类的析构函数定义为虚函数

基类指针指向子类对象,

如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确的释放

因此造成内存泄露

10、补充

  1. 多次调用delete

    这种情况语法检查并不会提示你,但是编译运行的过程中会出错

    例子: 两个指针指向同一块东西,释放空间的时候调用两次delete,但是不清楚这种情况算不算内存泄漏,就暂时写上吧。

    其实他也算new和delete没有配套使用,因为两个指针指向同一块东西,说明只调用了一次new。

  2. 分配内存的操作太多,没有来得及释放

    这种情况严格来说并不算内存泄漏,因为从逻辑上看并没有造成内存泄漏。

    但是如果一直调用new,会导致这样的情况:内存空间被分配完了,程序还没有来得及调用delete,会导致一些错误。

最后不专业的总结一下:

  1. new和delete没有配套使用,及衍生的各种情况
  2. 嵌套的情况,先释放了外层,内层的空间失联,及衍生的各种情况
  3. 两个指针指向同一个空间,导致的各种情况
  4. 一个指针指向了新的空间之前,没有释放之前的空间,及衍生的各种情况

继续阅读