由一道题引起的对内存泄露的思考
最近遇到了这么一道题:
看了这题目,首先第一感觉不由自主的就选了A。我们通常知道new/delete
与new[]/delete[]
当然需要配对使用,
否则的就会导致内存泄露。这是直观的感觉。
然而第一感觉却往往是不正确的。正确答案是B。瞬间模糊了自己对new/delete
的概念。虽说最近也在断断续续地看着
《深度探索C++对象模型》以及《Effective C++》,对new/delete
多少也有些了解。但这道题却让我对new/delete
的认知
一夜回到了解放前。于是决定透彻的了解下new/delete
的细节。
首先需要明确的概念是内存泄露(Memory Leak)的概念。
在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
可以这么说,假设我申请了一块1MB大小的地址,用一个ptr来指向该内存的首地址,但是在程序运行的时候不小心把ptr的值给覆盖掉了,所以现在这块内存的地址我无法获取,所以这块内存就相当于消失了。因为对于OS 的内存管理程序来讲,这块内存是有用的,但是对于程序而言,却缺乏该内存的首地址导致无法使用。
这么看来,我之前理解的new[]
来分配n块地址然后由delete[]
来回收n块地址,而delete
只能回收一块地址,所以导致内存泄露是完全不对的了。
内存泄露是指没有回收但指针却没了。如果delete
只是回收了第一块地址的话那么后续的地址没被回收,而且指针p[1],p[2]
仍然存在,这样的话也算不上是内存泄露啊。
那这样的话,为什么很多书上会讲new/delete
与new[]/delete[]
不匹配使用的话会导致内存泄露呢?
如果真的导致了内存泄露,那究竟是什么时候会泄露,什么时候不会泄露呢?
看来如果真想了解本质,必须知道new/delete
究竟干了什么事。
参考来自@Kelvin
大神的博文,我们来看看C++标准库的实现之一Clang的libcxx如何实现operator new/delete
由此看来,new/delete
不过是调用c函数库中系统函数malloc/free
而已。而对于new[]/delete[]
也类似
因此,new[]/delete[]
只不过是对new/delete
的一个调用而已。(Ps: 还是有区别的,其中编译器做了一些工作,下文会详细介绍)。
现在我们再看 char * p = new char[100]; delete p; 整个过程。 其本质是类似这样一种形式。
这样将的话也不会出现内存泄露的问题,那究竟什么时候能出现内存泄露呢?
另一个对new/delete
的class认知:
对于用户定义class类型,我们对new
的认知是分三步:
- 通过malloc来申请一块内存;
- 在内存上调用构造函数;
- 返回该class类型的指针。
相对的delete
是两部:
- 调用对应的析构函数;
- 将内存free掉。
那我们提出这样的假设,是不是用户定义的class类型的与内置基本类型的new[]/delete[]
不一致?
既然有疑问,就需要实验来验证下结果。
我们三组不同类型进行new[]/delete[]
来查看其内存模型。
这三组类型按照一下标准分类:
- 内置类型数组(int);
- POD类型数组;
- 成员函数包含指向堆的指针class数组,带有构造函数以及析构函数,我们称之为用户定义类型;
代码如下:
说明一下,malloc_stats()函数用来查看内存中malloc()申请的内存状况。
另外,程序运行到 delete complexStruct 的时候会segment fault。
我们主要是用gdb来跟踪,然后来查看内存中这些数组的数据。
编译的时候使用gdb -O0
来关闭优化,防止编译器将一些信息优化掉。
Ps. 每次运行时会将其余部分注释掉,只测试本部分的数据内存模型。
首先对于内置类型(int * ptrInt = new int [10]),在new[]
之后ptrInt的值是0x602010
,为了更方便的查看数据,我们将
里边的值都赋为0xcdcdcdcd
,同过x/8xg 0x602000
来显示从0x602000
开始的64bytes的内存如下:
其中有个问题是ptrInt的开始地址是0x602010
,但我们为什么要从0x602000
开始呢?
实际上,malloc/free
调用的时候每次传过来的指针有个头部信息,该信息一般用来存放32bit
的内存块信息,包括该块的大小以及是否空闲。我们这里的是0x00000031
,最后一位为1表明
该块被使用,剩下的0x00000030
表示该块大小为48个bytes。所以通过malloc
返回的指针实际上
不是你使用的内存的首地址,而是越过了8个字节的头部信息的位置,而free
的时候自动会将
指针回退8个字节来提取出该内存块的信息。一旦free
的时候找不到内存块信息则会出现segment fault的错误。(Ps. 最后一句话纯属个人理解)
下面看下POD类型数组的结构发现与内置类型的结果一模一样。
所以POD的new[]/delete
也不会导致内存泄露的问题。
接下来看一下用户定义的class数组。这一次p ptrComplexStruct
的时候发现地址不再是0x602010
,而成了0x602018
。
通过x/32xg 0x602000
来查看从0x602000
开始的256个bytes内存如下:
其中0x00000061
跟之前的0x00000031
一样是malloc出的内存块的头部,如果按照前种情况来看,说明对于new
来讲首地址应该是
0x602010
,而我们发现通过malloc出的内存块在头部之后又添加了一个8 字节的new
的头部,存放的内容是0xa
,也就是10,即该
数组的大小。之后从0x602018
开始才是真正的数据。这样的话也不難理解了。因为我们的class里边含有析构函数,所以delete[]
的
时候需要将数组中的每个object进行析构,而数组的个数就被存储在整个数组的开头部分,占8个bytes。
那class数组的结构有时包含数组长度,有时不包含数组长度,是通过什么判断的呢?
根据上边我们了解的delete[]
的过程中可以得知数组长度主要用途是为了delete[]
时进行析构的,所以我们假设结构的不同跟class是否
含有析构函数有关。
最终得证。如果把用户定义类型class的析构函数注释掉,则其内存模型中数组长度就不存在了。 而如果给POD类型添加一个析构函数,其内存模型也会添加一个数组长度。
好了,现在真相大白。总结一下, 普通的malloc申请的内存会包含一个头部信息存储块的大小以及使用信息,free通过读取这些信息来
进行回收,否则会出现segment fault
错误(个人理解), 而对于包含析构函数的的class,new[]
会对申请的块继续包裹一个头部信息
来存储数组的大小,delete[]
根据这个大小来一一进行析构。
而对于内存泄露,如果普通的内置类型以及class不包含动态分配的指针的情况下,利用new[]/delete
组合不会导致内存泄露,因为这本质
跟malloc/free
是一样的。而如果class中有new/malloc
动态分配的内存,如果new[]/delete
的话就会导致内存泄露。这是因为class内部的
内存本身应该由object的析构函数来回收内存,而由于delete
只会析构一个object,所以其他的objects成员的内存并没有得到回收,但
这些存放指针的内存却被回收了,从而导致了内存泄露。
另外,这个程序为什么会segment fault
呢?
这是由于对于g++来讲,new[]
出的内存第一个位置放的是数组长度,所以真正使用的内存的起始位置是在new[]
得到的指针后边8字节处,
而只有delete[]
才能读懂这种模型,它会将指针向前回退8个字节,然后将指针传给free
, 之后free
会继续回退8字节来查看该内存块的信息。
但是delete
的话只能将当前位置的指针传给free
,free
通过回退8字节后的位置实际上是数组的长度信息,而此时由于free
获取不到有用的
内存信息从而产生segment fault
的错误。
26 May 2016
Post by: MetaCoder