首页 > 编程知识 正文

内存中的堆栈和数据结构堆栈,栈内存堆内存方法区内存

时间:2023-05-04 07:03:35 阅读:201096 作者:2140

前言:我们经常听见一个概念,堆(heap)和栈(stack),其实在数据结构中也有同样的这两个概念,但是这和内存的堆栈是不一样的东西哦,本文也会说明他们之间的区别的,另外,本文的只是是以C/C++为背景来说明,不同的语言在内存管理上面会有区别。本文是第二篇,介绍内存中的堆与栈。
 

一、C++中的内存概述

1.1 内存的分类标准——五分类

在C++中,内存分成5个区,他们分别是堆,栈,自由存储区,全局/静态存续区,常量存续区
(1)栈:内存由编译器在需要时自动分配和释放。通常用来存储局部变量函数参数,函数调用后返回的地址。(为运行函数而分配的局部变量、函数参数、函数调用后返回地址等存放在栈区)。栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(2)堆:内存使用new进行分配,使用delete或delete[]释放。如果未能对内存进行正确的释放,会造成内存泄漏。但在程序结束时,会由操作系统自动回收。
(3)自由存储区:使用malloc进行分配,使用free进行回收。
(4)全局/静态存储区:全局变量静态变量被分配到同一块内存中,C语言中区分初始化和未初始化的,C++中不再区分了。(全局变量、静态数据 存放在全局数据区)
(5)常量存储区:存储常量,不允许被修改。
 

还有一些资料是将内存分为三类,如下。

1.2 内存的分类标准——三分类
  这里,在一些资料中是这样定义C++内存分配的,可编程内存在基本上分为这样的几大部分:静态区、堆区、栈区。他们的功能不同,对他们使用方式也就不同。
(1)静态(全局)存储区——static:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。也是程序结束后,由操作系统释放。
(2)栈区——stack:在执行函数时,函数参数,局部变量(包括const局部变量),函数调用后返回的地址都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3)堆区——heap:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或 delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。

1.3 内存的分类标准——另一种五分类

(1)栈又叫堆栈,非静态局部变量/函数参数/返回值等等 ,还有每次调用函数时保存的信息。每当调用一个函数时,返回到的地址和关于调用者环境的某些信息的地址,比如一些机器寄存器,就会被保存在栈中。然后,新调用的函数在栈上分配空间,用于自动和临时变量。

2.内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。

 3.堆用于程序运行时动态内存分配,堆是可以上增长的。堆区域从BSS段的末尾开始,并从那里逐渐增加到更大的地址。堆是由程序员自己分配的。堆区域由所有共享库和进程中动态加载的模块共享。

4.数据段分为初始化数据段和未初始化数据段。初始化的数据段,通常称为数据段,是程序的虚拟地址空间的一部分,它包含有程序员初始化的全局变量和静态变量,可以进一步划分为只读区域和读写区域。未初始化的数据段,通常称为bss段,这个段的数据在程序开始之前有内核初始化为0,包含所有初始化为0和没有显示初始化的全局变量和静态变量。

5.代码段也叫文本段,是对象文件或内存中程序的一部分,其中包含可执行代码和只读常量。文本段在堆栈的下面,是防止堆栈溢出覆盖它。,通常代码段是共享的,对于经常执行的程序,只有一个副本需要存储在内存中,代码段是只读的,以防止程序以外修改指令。
 

1.4 内存的分类标准——四分类

简单的介绍一下四个区域:

(1)代码区--------主要存储程序代码指令,define定义的常量。

(2)全局数据区------主要存储全局变量(常量),静态变量(常量),常量字符串。

(3)栈区--------主要存储局部变量,栈区上的内容只在函数范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但内存大小有限。

(4)堆区--------由malloc,calloc分配的内存区域,其生命周期由free决定。堆的内存大小是由程序员分配的,理论上可以占据系统中的所有内存。
四分类如下所示:

个人偏好,四分类更好理解一些。注意这里的一些什么 .bss  .data这些代表什么含义。

总结:

Stack memory内存,自动分配和释放,内存空间有限;Heap Memory内存,手动分配和释放,空间是很大,几乎没有空间限制。  

1.5 函数调用后返回地址——保存在栈内存上

函数调用时通过一个指向函数的指针指向函数,函数返回时将回归到调用处,那个地方就是函数调用结束后返回地址

另外返回地址保存在栈上,最先调用的函数最早入栈,最后出栈,而最后调用的函数最后入栈,最先出栈。
 

二、关于内存栈(memory stack)

2.1 栈溢出

“栈”由程序自动向操作系统申请分配以及回收,速度快,使用方便,但程序员无法控制。但是栈的空间很小,只要栈的剩余空间大于所申请空间,系统将为程序提供内存,若需要分配的空间大于栈内存,则分配失败,则提示栈溢出错误。

#include <iostream>int main(){    int i = 10; //变量i储存在栈区中    const int i2 = 20; //const局部变量也存储在stack    int i3 = 30;    std::cout << &i << " " << &i2 << " " << &i3 << std::endl;    return 0;}/*运行结果为: 0x28fedc  0x28fed8 0x28fed4  16进制地址,递减的*/

注意:const局部变量也储存在栈区内,栈区向地址减小的方向增长。


2.2 栈的特性

(1)申请大小的限制
       在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

(2)栈的申请效率很高
栈由系统自动分配,速度较快。但程序员是无法控制的,结束后由操作系统进行释放。

(3)栈使用的过程
       栈在函数调用时,

第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
 

三、关于内存堆(memory heap)

3.1 堆溢出

       堆是向高地址扩展的数据结构,是不连续的内存区域,这是由于系统使用链表存储空闲内存地址的,自然是不连续的。而链表的遍历方式是由低地址向高地址,堆的大小受限于计算机系统中有效的内存,由此可见,堆获得的空间比较灵活,也比较大。
       程序员向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。分配的速度较慢,地址不连续,容易碎片化。此外,由程序员申请,同时也必须由程序员负责销毁,否则导致内存泄露
 

3.2 堆的特性
       操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

(1)申请大小的限制
      堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的物理内存。由此可见,堆获得的空间比较灵活,也比较大。
(2)堆的使用效率
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

(3)堆使用的过程
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
 

总结:

栈内存:由高地址向低地址,连续,快速,空间小;

栈内存:由低地址向高地址,不连续,缓慢,空间大。

可以列举成下面的表格:

 申请方式内存大小使用效率存储内容栈(stack)自动申请释放高效 堆(heap)手动申请释放缓慢 

 

四、为什么C,C++在传递数组的时候传递的是地址或者是引用呢?

从上面的分析可以知道,由于函数的参数存储在栈内存中,所以如果一个数组比较大,我传递一个很大的数组,如果复制的是数组的值到形参上面,必然会导致栈内存不够用,即“栈溢出”,所以只传递一个地址值,而不传递实际的值就可以避免这一问题,其实向java,C#这些语言也有着相同的底层原理,后面继续说明。

总结:

我们在C语言里面函数的参数传递经常分为,值传递/指针传递

在C#,Java,C++里面函数的参数传递我们经常分为,值传递/引用传递

其实从内存的“栈内存”角度来说,所有的函数参数传递都只有一种形式,那就是值传递。

因为如果参数是值,则会拷贝栈里面的值到函数的形参里面去,这当然是值传递了,

如果参数是地址或者是引用,其实同样是会拷贝存在在栈区域里面的地址或者是引用传递到函数形参,只不过这个地址或者是引用不是真正的数据,拷贝的那个相同的地址或者谁引用会指向同一段数据,所以我们称之为传递引用或者是地址。

总而言之:传递地址或者是引用只是表面,本质都是“值传递”。

个人理解,如果大神有更好地理解,希望可以分享告知,谢谢。想要彻底弄懂这些概念,还是结合几篇文章一起看更好理解。

 

五、C#的内存管理

详细可以参考我的另外一篇文章:

一文读懂C#的 堆、栈、值类型、引用类型

C# (CLR) 中内存分配解析

 

 

 

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。