堆(heap)和栈(stack)的区别

简介: 堆(heap)和栈(stack)的区别

在学习C语言时,就对二者有过一面之交,当时并没有认真的去寻其根源,只知道简单的内存分配,须知现今学习Java的虚拟机这两个名词多次出现在眼前,不如就此机会彻底搞懂。

——答案摘自Google和Stack Overflow


1,首先来一句先入为主的总结:


栈是用于静态内存分配,堆用于动态内存分配,二者都存储在计算机的RAM中。


2,栈的概念


  • 分配在栈上的变量在编译期直接分配在内存中,对该内存的访问非常快。当一个函数或方法调用另一个函数,这个函数又调用了第三个函数,以此类推。所有的函数保持暂停状态直到最后一个函数快速地返回值;
  • 堆栈始终以后进先出(LIFO)顺序保留,最近保留的块始终是要释放的下一个块。 这使得跟踪堆栈非常简单,从堆栈中释放块只不过是调整一个指针。


3,堆的概念


  • 分配在堆上的变量是在运行时分配了内存,对该内存的访问有点慢,但堆大小仅受虚拟内存大小的限制。堆的元素彼此之间没有依赖关系,并且可以随时随机访问。
  • 可以随时分配一个块并随时释放它。因为无序,使得在任何给定时间跟踪堆的哪些部分被分配或释放变得更加复杂。


4,二者使用的区别


  • 如果确切地知道在编译之前需要分配多少数据并且数据量不是太大,则可以使用栈。如果不确切知道运行时需要多少数据,或者需要分配数据量较大,则可以使用堆。
  • 在多线程情况下,每个线程都有自己完全独立的栈,但它们将共享堆。栈是特定于线程,而堆是特定于应用程序。在异常处理和线程执行中,栈很重要。
  • 来一张显然意见的图来区分二者的不同:

1665663293908.jpg

堆和栈的鲜明的特征


栈:

  • 像堆一样存储在计算机RAM中
  • 在堆栈上创建的变量超出范围会自动取消分配
  • 与堆上的变量相比,分配要快得多
  • 使用实际的栈数据结构实现
  • 存储本地数据,返回地址,用于参数传递
  • 当使用过多的堆栈时(主要来自无限或太深的递归,非常大的分配),可能会出现栈溢出
  • 可在无指针的情况下使用在栈上创建的数据
  • 能够确切地知道在编译时需要分配多少数据并且它不是太大
  • 通常在程序启动时已确定分配内存的最大值


堆:

  • 像栈一样存储在计算机RAM中
  • 在C++中,必须手动销毁堆上的变量,并且永远不会超出范围。使用delete,delete []或free释放数据
  • 与栈上的变量相比,分配更慢
  • 按需分配程序使用的数据块
  • 当存在大量分配和解除分配时,可能会出现碎片
  • 在C++或C中,在堆上创建的数据将由指针指向,并分别用new或malloc分配
  • 如果请求分配的缓冲区太大,可能会出现分配失败
  • 如果不确切知道运行时需要多少数据,或者需要分配大量数据,则可以使用堆
  • 负责内存泄漏


代码示例:


int foo()
{
  char *pBuffer; //<--nothing allocated yet (excluding the pointer itself, which is allocated here on the stack).
  bool b = true; // Allocated on the stack.
  if(b)
  {
    //Create 500 bytes on the stack
    char buffer[500];
    //Create 500 bytes on the heap
    pBuffer = new char[500];
   }//<-- buffer is deallocated here, pBuffer is not
}//<--- oops there's a memory leak, I should have called delete[] pBuffer;

底层堆和栈的实现原理


1,栈:


  • 栈通常与CPU上的特殊寄存器紧密串联,命名为栈指针。初始化时,栈指针指向栈的顶部(栈中的最高地址)。
  • CPU具有将值压入栈并从栈中弹出的特殊指令。每次push操作都将值存储在栈指针的当前位置,并减少栈指针的值。 pop操作会回收栈指针指向的值,然后增加栈指针的值。存储和回收的值是CPU寄存器的值。
  • 当调用函数时,CPU使用特殊指令来推送当前指令指针,即在栈上执行代码的地址。然后CPU通过将指令指针设置为被调用函数的地址来跳转到该函数。稍后,当函数返回时,从栈中弹出旧的指令指针,并在调用函数后立即执行代码。
  • 输入函数时,栈指针值会减少,以便在栈上为本地(自动)变量分配更多空间。如果函数有一个本地32-bit变量,则在栈上留出四个字节。当函数返回时,栈指针被移回以释放分配的区域。
  • 如果是带参函数,则在调用函数之前将这些参数压入栈。然后,函数中的代码能够从当前栈指针向上追踪栈来找到这些值。
  • 嵌套函数调用很优雅。每次调用都将分配函数参数,为局部变量返回地址和空间,这些追踪记录可以堆叠用于嵌套调用,并在函数返回时以正确的方式展开。
  • 由于栈是有限的内存块,因此可以调用太多的嵌套函数或为局部变量分配太多空间会导致栈溢出。通常,栈的存储区域的设置方式是在栈底部(最低地址)写入能够触发CPU中的陷阱或异常。然后,运行时可以捕获此异常并将其转换为某种栈溢出异常。

image.png

2,堆


  • 堆包含已使用和可用块的链状列表。通过从其中一个空闲块创建合适的块来满足堆上的新分配(new或malloc),这需要更新堆上的块列表。有关堆上块的元信息存储在堆中,位于每个块的前面。
  • 随着堆增长,块的地址从较低地址增加到较高地址。因此,可以将堆视为一堆内存块,这些内存块在分配内存时会增大。如果堆对于分配而言太小,可以从底层操作系统获取更多内存来增加大小。
  • 分配和回收许多小块可能使堆处于这样的状态:在所使用的块之间散布有许多小的空闲块。分配大块的请求可能失败,因为即使空闲块的组合大小足够大,也没有任何空闲块足够大以满足分配请求,这称为堆碎片。可以参考一下垃圾收集算法中最基本的“标记-清除”算法。
  • 当释放与空闲块相邻的使用块时,可以将新的空闲块与相邻的空闲块合并以创建更大的空闲块,从而有效地减少堆碎片。

image.png


Q&A


1,为什么栈的速度快?

栈速度快的原因有三点:其一是当进程启动时由操作系统分配栈(假设存在操作系统),它由程序保持内联。 这是栈速度更快的另一个原因 - 推送和弹出操作通常是一个机器指令,而现代机器可以在一个周期中至少完成3个,而分配或释放堆涉及调用OS(操作系统)代码;其二是访问模式使得从中分配和释放内存变得微不足道(指针/整数简单地递增或递减),而堆在分配或释放中涉及更复杂的簿记。 第三点是栈中的每个字节都经常被频繁地重用,这意味着它往往被映射到处理器的缓存,使其非常快。 堆的另一个性能损失是堆(主要是全局资源)通常必须是多线程安全的,即每个分配和释放通常需要与程序中的“所有”其他堆访问同步。


下图是堆和栈的示意图:

image.png

2,什么决定二者的大小?

创建线程时设置堆栈的大小。堆的大小在应用程序启动时设置,但可以在需要空间时增长(分配器从操作系统请求更多内存)。


3,二者的作用域?

栈附加到一个线程,因此当线程退出时栈将被回收。 堆通常在应用程序启动时由运行时分配,并在应用程序(技术过程)退出时回收。


4,二者在多大程度上受操作系统或语言运行时控制?

操作系统在创建线程时为每个系统级线程分配栈。通常,语言运行时调用操作系统来为应用程序分配堆。


相关文章
|
2月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
160 77
|
11天前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
26 11
|
25天前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
2月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
48 7
|
4月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
393 9
|
4月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
66 1
|
2月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
54 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
2月前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
52 9
|
4月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
107 5
|
4月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
137 21