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做的事情有三个:
- 分配内存
- 指针转型
- 调用构造函数
其中第二个这里不讨论,然后再看delete做的事情:
- 调用析构函数
- 释放内存
接下来看例子:
我们定义拥有三个指针的指针数组,每个指针指向一个class(下图左半部分)
每个class里面包含指针,指向了其他地方(下图右半部分)
我们使用 delete [] 的时候,编译器知道,我要释放三个class,
然后依次调用三个析构函数,将右半部分的空间释放了
然后就是释放左半部分的空间。
这是正确的做法。
还是刚刚一样结构的指针数组,
此时我们使用 delete ,编译器以为,自己要释放一个class
然后就只调用了第一个class的析构函数(右半部分空框框的空间被释放了)
但是其余两个class并没有调用析构函数(右半部分问号框框的空间没被释放)
然后编译器就把左半部分的空间给释放了
导致那两个class的空间无法控制了,造成了内存泄露
这是错误的做法
理解之后,我感觉这种内存泄漏有种 2 3 结合的意思:
因为嵌套导致了class的new和delete不配套
说这么多的原因是提醒大家不要对 delete[] 造成误解,
用不用delete的方括号,影响的并不是数组本身的空间(左半部分),数组只要调用了delete就会被释放
他影响的主要是数组内容指向的空间(右半部分),可能导致因为没有调用析构函数而造成内存泄露
(这一部分主要参考侯捷的视频讲解,视频讲解地址,这个视频并不只是讲了这些东西,这部分是在视频的42分钟开始讲的,上面两个图也是视频中的截图)
5、指向对象的指针数组不等同于对象数组
这种泄露有点像第四种,
- 对象数组:数组的每个元素是对象本身
- 指向对象的指针数组:数组的每个元素是指针,每个指针指向的是对象
小窍门: 理解各种相似名字的时候,先不要看他的修饰词,就只看名词本身。
指向对象的指针数组:他就是一个数组,数组的每个元素是指针
数组指针:他就是一个指针,是指向数组的指针
针对对象数组:
释放的时候,直接 delete[] 就可以,刚刚讨论过了
针对指向对象的指针数组:
直接 delete[] 是不行的,因为他释放的是指针的空间,指针指向的空间并没有释放(析构函数没有调用)
应该先遍历数组,依次调用每个元素的析构函数
最后使用 delete[],将数组中的每个指针释放了
6、缺少拷贝构造函数
这部分主要参考侯捷视频,视频链接,是从22分钟开始将这部分的内容
缺少的意思并不是没有,系统有默认的,但是默认的并不能达到所有要求,这时候就必须自己再写一个
这种泄露是因为两次调用了delete,
两次释放相同的内存是一种错误的做法,同时可能会造成堆的奔溃。
这里,我分了三种情况(主要讨论后面两种情况)
- 按值传递(默认、自己写)
- 指针传递(默认)
- 指针传递(自己写)
1. 按值传递(默认、自己写)
这种情况,拷贝构造函数不管是默认的还是自己写的,都是将值复制一份,然后给class,没什么说的
2. 指针传递(默认)
这种的话,可以解决大部分情况,但是也有不能解决的情况。
假设我们有这一个class a,他内部有个指针,指向了hello,已经分配过了空间
这时候,我们new一个class b,让他等于class a,如果没有写新的拷贝构造,他就会调用默认的,
默认的他只一个功能,一个对一个的复制。最终会变成这样
两个class里面的指针都指向了hello这个空间
一旦这时候,调用了class a或者class b的析构函数,hello所在的空间会被释放,
这时候,另一个class的指针虽然还是指向那个空间,但是,内容已经没有了,这种指针有一个名字 “悬空指针”
不管之后是使用这个指针,还是调用析构函数释放这个指针,都会引发不可预知的错误。
这就是内存泄露
这种形式的拷贝也有一个名字 “浅拷贝”
3. 指针传递(自己写)
发现了默认拷贝构造函数的问题,我们就需要自己写一个。
过程:
- 给class b的指针分配空间
- 使用复制函数(strcpy),将class a中指针指向的hello复制给class b一份。
执行之后是这样的
这样的就没有内存泄露
这种方式的拷贝叫 “深拷贝”
7、缺少重载赋值运算符
这种情况与 6 相似,是因为默认的赋值运算符,并没有特殊的操作。比如下面这种
执行之后,P1原来指向的空间,因为没有调用delete,导致失控
而且,赋值之后,P1和P2指向了同一块空间,这种情况刚刚讨论过,也会导致内存泄漏
8、关于nonmodifying运算符重载的常见迷思
这个我不理解,照抄了
- 返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针
- 返回内部静态对象的引用。
- 返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收
解决这一类问题的办法是重载运算符函数的返回值不是类型的引用,二应该是类型的返回值,即不是 int&而是int
9、没有将基类的析构函数定义为虚函数
基类指针指向子类对象,
如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确的释放
因此造成内存泄露
10、补充
-
多次调用delete
这种情况语法检查并不会提示你,但是编译运行的过程中会出错
例子: 两个指针指向同一块东西,释放空间的时候调用两次delete,但是不清楚这种情况算不算内存泄漏,就暂时写上吧。
其实他也算new和delete没有配套使用,因为两个指针指向同一块东西,说明只调用了一次new。
-
分配内存的操作太多,没有来得及释放
这种情况严格来说并不算内存泄漏,因为从逻辑上看并没有造成内存泄漏。
但是如果一直调用new,会导致这样的情况:内存空间被分配完了,程序还没有来得及调用delete,会导致一些错误。
最后不专业的总结一下:
- new和delete没有配套使用,及衍生的各种情况
- 嵌套的情况,先释放了外层,内层的空间失联,及衍生的各种情况
- 两个指针指向同一个空间,导致的各种情况
- 一个指针指向了新的空间之前,没有释放之前的空间,及衍生的各种情况