内存的来源(堆、栈)与内存管理—HJ-record04

目录

 

内存的来源

栈(Stack)

堆(heap)

举例说明

内存中创建的各种objects的生命周期 

stack objects的生命期

static local objects 的生命期

global objects的生命期

heap objects 的生命期

动态内存分配及管理(new与delete)

new:先分配memory,再调用ctor

delete:先调用dtor,再释放memory

内存在底层的存储调配方式 

动态分配所得的内存块(在vc底下的编译器)

动态分配所得的array(在vc底下的编译器)

内存泄漏

array new 一定要搭配array delete来释放


内存的来源

栈(Stack)

Stack ,是存在于某作用域 (scope)  的一块內存空间(memory space) 。 例如当你调用函数 , 函数本身即会形成一个stack,用來放置它所接收的参数、local object(临时对象)以及返回地址。

在函数本体 (function body)  內声明的任何变量 ,其所使用的內存块都取自上述的stack 。

堆(heap)

Heap,或称为 system heap,是指由操作系统提供的一块 global内存空间,程序可动态分配 (dynamic allocated) 从某中获得若干区域 (blocks),那怎么叫动态取呢?就是用关键字"new"来取。

举例说明

下面的“c1(1,2);”所创建的时候,所占用的空间就是栈上的,同理,用New关键字创建新的对象,所占用的就是堆上,与跟在栈上相比,在堆上动态创建的时候,还需要手工的进行释放(detete)掉。

内存中创建的各种objects的生命周期 

stack objects的生命期

其中,c1便是所谓的stack object,其生命在作用域(scope)结束之际便结束了,这种作用域内的object,又称为auto object,因为它会被自动的清理。

static local objects 的生命期

c2便是所谓static object,其生命在作用域(scope)结束之后仍然存在,直到整个程序结束。

global objects的生命期

c3便是所谓的global object,其生命在整个程序结束之后才结束。也可以把其看作一种static object,其作用域是整个程序。

heap objects 的生命期

p所指的便是heap object,其生命在它被deleted的时候便结束了。如果不执行delete操作,比如这样写:

以上出现内存泄漏(memory leak),因为当作用域结束,p所指的heap object仍然存在,但指针p的生命却结束了,作用域之外再也看不懂p(也就没机会delete p)。

动态内存分配及管理(new与delete)

new:先分配memory,再调用ctor

new在执行的时候,被编译器分解为三个动作:

  • 第一步:先分配内存(内部调用malloc(n),在堆里面分配内存);
  • 第二步:再将第一步得到的指针,给转换一下类型(将新建的内存空间"void*"类型转为对应的"Complex*"类型);
  • 第三步:通过第二步得到的指针,调用构造函数,初始化对象;

delete:先调用dtor,再释放memory

delete执行的顺序和new的执行的顺序是正好相反的,分两步执行:

  • 第一步:调用析构函数,要把对象所存放的内容给清理掉,如果对象是一个字符串的话,字符串本身其实就是一个指针而已;
  • 第二步:再进行释放内存,把对象本身给清理掉。

内存在底层的存储调配方式 

动态分配所得的内存块(在vc底下的编译器)

其实,C++中进行的动态内存分配,其本质都是调用的C语言中的malloc函数和free函数,只不过又再其上面添加了一层而已。那么在动态分配中,一个变量在内存块中到底是分配了多大的字节呢?下面来进行细究:

在debug模式下,创建一个复数类,和在relese模型下创建复数类,编译器所分配的内存是不一样的!而且,有很大的不同!

上图中分左右两大组,左边表示分配一个复数类对象时候的内存对象分配情况,右边表示分配一个字符串类对象的内存分配实际情况,以左边这组为例,大组里面又可以细分为左右两个部分,左边部分表示是在调试(debug)状态下的内存分配,右边部分是表示非调试状态下的内存分配(release)。一比对可以清晰的发现,调试与非调试的不同状态对应的内存存储情况也是完全不同的!

以创建复数类的分配内存空间为例:

上图中每一小横格所占的内存空间都是4字节。

灰色横格,是在debug模式下,编译器特意留足的,其大小为:(4*7+4*1),再分配给真正定义复数对象的内存空间(对应绿色横格段)的上面(分配7格)和下面(分配一格) 

Cookie的作用(对应图中砖色横格):要来计算整块分配内存空间的长度。因为当编译器回收的时候,用户是只提供一个指针的,编译器是不知道具体回收多大的内存,所以,才有了Cookie,用来计算每次分配多大的内存,而且记录分配的内存具体到那个位置,方便回收的时候好回收。

分配给内存的空间大小必须是16的倍数。不够的需要填pad(蓝色横格)来充够分配的内存空间是16的倍数,因为这样可以保证最后的一个bit位的都是零。比如在复数类的debug模式下,正常算下来是52个字节就够了,但因为不是16的倍数,必须加3个pad(3*4),才能凑够(52+3*4=64)。

需要注意的是,只有在debug模式下,才有灰色的横格,如果在非调试模式下,就不会有灰色的横格了!

这便是在编译器最底层所实际分配的内存情况!

动态分配所得的array(在vc底下的编译器)

那么在动态分配的情况下,在调试模式下,会分配对应的Debugger Header(橙色横格,32+4),对应的存放数据的部分是灰色横格,并且每个数据占两个,因为一个横格是4字节,而double类型的数据是8字节,所以一个数据要分配两格((4*2)*3),在算上开头和结尾的两个Cookie(无颜色填充、4*2),这里有个特殊的地方,还需要在灰色横格前面加了一格用于存放数组的长度(4),这样加起来就是72个字节了,同样,必须保证总字节数为16的倍数,要添加2个pad(蓝色空格,2*4),凑够80.

在非调试模式下,就没有了Header这部分了。其他的都一样了就。

内存泄漏

array new 一定要搭配array delete来释放

先明白前提:动态创建的字符串数组,字符串的本质是一个指针,这个指针指向的内存空间是字符串的内容,所以,这个数组中每个位置存放的都是指针,

这是因为,如果在delete的时候("delete[] p;"),加中括号("[]")意味着new创建几个变量出来,就调用几次析构函数去清除掉新创建出来的对象;不加中括号("[]")的话,意味着只会调用一次析构函数去,剩下的变量就没法清除了!

这里又要说的细一些,回顾之前的delete执行的过程,分两步执行:第一步,把变量中存放的数据(指针)杀掉;第二步,把这个变量本身杀掉。那么,不加中括号导致的问题是发生在第一步,而并不会影响第二步。不加中括号的话,导致在第一步的时候变量中存放的指针所指向的内存空间没有被杀干净,紧接着第二步,编译器根据Cookie,来把变量本身杀掉,也把存放于此的指针变量本身给杀了,那指针变量所指向的内存空间就被泄漏了,这样就造成了内存的泄漏

所以,严正强调的是可能人人都知道会引起内存泄漏,但很少人能想到内存泄漏发生的其实是第一步,而不是第二步,或者而非整块内存空间的丧失。

最后还需要强调的是,这种情况是因为动态创建的数组里面存放的是指针所造成的,如果是复数类,即数组中放的不是指针,而是数据,那不加中括号"[]"也是可以的。但是,我们还是要养成一个好的编程习惯,来确保编程的万无一失!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值