什么是堆栈和堆?


8099

编程语言书籍解释了值类型是在堆栈上创建的,而引用类型是在堆上创建的,而没有说明这两个是什么。我还没有阅读清楚的解释。我了解堆栈是什么。但,

  • 它们在哪里和在哪里(物理上在真实计算机的内存中)?
  • 它们在多大程度上受操作系统或语言运行时的控制?
  • 他们的范围是什么?
  • 什么决定了它们的大小?
  • 是什么使速度更快?

175
可以在这里找到一个很好的解释堆栈和堆有什么区别?
Songo

12
同时(真)好:codeproject.com/Articles/76153/...(栈/堆部分)


3
相关内容,请参见堆栈冲突。堆栈冲突的修复影响了系统变量和行为(例如)的某些方面rlimit_stack。另请参阅Red Hat Issue 1463241
jww

3
@mattshane堆栈和堆的定义不依赖于值和引用类型。换句话说,即使值和引用类型从不存在,也可以完全定义堆栈和堆。此外,当理解值和引用类型时,堆栈只是实现细节。Per Eric Lippert:堆栈是一个实现细节,第1部分
马修(马修)

Answers:


5961

堆栈是为执行线程预留的暂存空间。调用函数时,将在堆栈顶部保留一个块,用于存放局部变量和一些簿记数据。当该函数返回时,该块将变为未使用状态,并且可以在下次调用该函数时使用。堆栈始终按LIFO(后进先出)顺序保留;最近保留的块始终是下一个要释放的块。这使得跟踪堆栈非常简单。从堆栈中释放一个块无非就是调整一个指针。

堆是为动态分配预留的内存。与堆栈不同,堆中的块分配和释放没有强制的模式。您可以随时分配一个块,并随时释放它。这使得跟踪在任何给定时间分配或释放堆的哪些部分变得更加复杂。有许多自定义堆分配器可用于调整不同使用模式的堆性能。

每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管对于不同类型的分配有多个堆并不少见)。

要直接回答您的问题:

它们在多大程度上受操作系统或语言运行时的控制?

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

他们的范围是什么?

堆栈连接到线程,因此当线程退出时,堆栈将被回收。通常,堆是在运行时在应用程序启动时分配的,并在应用程序(技术上已退出)退出时被回收。

什么决定了它们的大小?

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

是什么使速度更快?

堆栈速度更快,因为访问模式使从中分配内存和取消分配内存变得微不足道(指针/整数只是递增或递减),而堆的分配或释放则涉及到更为复杂的簿记工作。而且,堆栈中的每个字节都倾向于被非常频繁地重用,这意味着它倾向于被映射到处理器的高速缓存中,从而使其非常快。堆的另一个性能损失是,堆(通常是全局资源)通常必须是多线程安全的,即,每个分配和释放都必须(通常)与程序中的“所有”其他堆访问同步。

清晰的演示:
图片来源:vikashazrati.wordpress.com


74
好的答案-但我认为您应该补充一点,虽然进程启动时由操作系统分配堆栈(假设存在操作系统),但程序会内联地维护堆栈。这也是堆栈也更快的另一个原因-推送和弹出操作通常是一条机器指令,现代机器可以在一个周期中至少执行3条指令,而分配或释放堆则涉及调用OS代码。
sqykly

273
最后的图让我真的很困惑。我以为我明白了,直到我看到那个图像。
新浪马达尼

10
@Anarelle处理器在有或没有os的情况下运行指令。我心中最接近的一个例子是SNES,它没有API调用,也没有我们今天所知道的OS-但它具有堆栈。在这些系统上分配堆栈是加法和减法,这对于通过从创建它们的函数返回而弹出它们时销毁的变量是很好的,但应将其解释为一个构造函数,其结果不能只是扔掉。为此,我们需要不与调用和返回绑定的堆。大多数操作系统都有API堆,没有理由自己做
狡猾地

2
“堆栈是保留为暂存空间的内存”。凉。但是就Java内存结构而言,它实际上在哪里?是堆内存/非堆内存/其他(根据betsol.com/2017/06/…的 Java内存结构 )
Jatin Shashoo

4
@JatinShashoo作为字节码解释器的Java运行时增加了一层虚拟化,因此您所指的只是Java应用程序的观点。从操作系统的角度来看,这仅仅是一个堆,Java运行时进程在其中将其一些空间分配为处理的字节码的“非堆”内存。该OS级堆的其余部分用作存储对象数据的应用程序级堆。
kbec

2348

堆:

  • 就像堆一样存储在计算机RAM中。
  • 在堆栈上创建的变量将超出范围并自动释放。
  • 与堆中的变量相比,分配要快得多。
  • 用实际的堆栈数据结构实现。
  • 存储用于参数传递的本地数据,返回地址。
  • 当使用过多的堆栈时(可能来自无限或太深的递归,非常大的分配),可能会导致堆栈溢出。
  • 可以在没有指针的情况下使用在堆栈上创建的数据。
  • 如果您确切知道在编译之前需要分配多少数据并且它不会太大,则可以使用堆栈。
  • 通常,程序启动时已经确定了最大大小。

堆:

  • 像堆栈一样存储在计算机RAM中。
  • 在C ++中,必须手动销毁堆上的变量,并且切勿超出范围。该数据被释放与deletedelete[],或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;

31
指针pBuffer和b的值位于堆栈上,并且很可能在函数的入口处分配。根据编译器的不同,缓冲区也可以分配在函数入口处。
安迪

36
常见的误解是CC99语言标准定义的语言(可在open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf上获得)需要“堆栈”。实际上,“堆栈”一词甚至没有出现在标准中。这C通常会回答wrt / 的堆栈使用情况的语句是正确的,但绝不是该语言所必需的。有关更多信息,请参见knosof.co.uk/cbook/cbook.html,尤其是如何C在诸如en.wikipedia.org/wiki/Burroughs_large_systems之
johne

55
@Brian您应该解释为什么在堆栈上创建buffer []和pBuffer指针,以及为什么在堆上创建pBuffer的数据。我认为某些ppl可能会使您的答案感到困惑,因为他们可能认为该程序专门指示在栈与堆之间分配内存,但事实并非如此。是因为Buffer是值类型,而pBuffer是引用类型?
Howiecamp

9
@Remover:没有指针保存地址,它可以平等地指向堆或堆栈上的某物。new,malloc和其他一些与malloc类似的功能在堆上分配并返回分配的内存的地址。为什么要在堆上分配?这样您的内存就不会超出范围并被释放,直到您想要它。
布莱恩·邦迪

35
“负责内存泄漏”-堆不负责内存泄漏!懒/健忘/前Java编码员/编码员!
Laz 2013年

1370

最重要的一点是,堆和堆栈是内存分配方式的通用术语。它们可以以许多不同的方式实现,并且这些术语适用于基本概念。

  • 在一堆物品中,物品按照放置在一个物品上的顺序排列在另一个物品的顶部,并且您只能删除顶部的物品(不能将整个物品翻倒)。

    像纸堆一样堆

    堆栈的简单性在于,您不需要维护一个表,该表包含分配的内存的每个部分的记录。您唯一需要的状态信息是指向堆栈末尾的单个指针。要分配和取消分配,您只需递增和递减该单个指针。注意:有时可以将堆栈实现为从内存部分的顶部开始,然后向下扩展而不是向上扩展。

  • 在堆中,项的放置方式没有特定的顺序。因为没有清晰的“顶部”项目,所以您可以按任意顺序进入和移除项目。

    像一堆甘草一样堆

    堆分配需要完整记录分配的内存和未分配的内存,并进行一些开销维护以减少碎片,找到足够大以适合请求的大小的连续内存段,依此类推。可以随时释放内存以留下可用空间。有时,内存分配器将执行维护任务,例如通过移动分配的内存来对内存进行碎片整理或进行垃圾回收-在运行时识别内存不再在作用域中并对其进行分配。

这些映像在描述堆栈和堆中分配和释放内存的两种方式方面应该做得相当不错。好吃

  • 它们在多大程度上受操作系统或语言运行时的控制?

    如前所述,堆和堆栈是通用术语,可以通过多种方式实现。计算机程序通常具有称为调用堆栈的堆栈,该堆栈存储与当前功能相关的信息,例如指向从哪个函数调用的指针以及任何局部变量。由于函数先调用其他函数然后返回,所以堆栈会不断扩大和缩小,以保存来自函数的信息,直到调用堆栈为止。一个程序实际上并没有对它的运行时控制。它由编程语言,操作系统甚至系统架构决定。

    堆是用于动态且随机分配的任何内存的通用术语。即乱序。内存通常由OS分配,应用程序调用API函数进行此分配。管理动态分配的内存需要相当大的开销,通常由所使用的编程语言或环境的运行时代码来处理。

  • 他们的范围是什么?

    调用栈是一个低级概念,从编程的意义上讲它与“作用域”无关。如果您分解一些代码,则会看到指向堆栈部分的相对指针样式引用,但是就高级语言而言,该语言强加了自己的作用域规则。但是,堆栈的一个重要方面是,一旦函数返回,该函数本地的所有内容都会立即从堆栈中释放出来。鉴于您的编程语言是如何工作的,这种工作方式与您期望的一样。在堆中,也很难定义。范围是操作系统公开的任何内容,但是您的编程语言可能会添加有关其在应用程序中的“作用域”的规则。处理器架构和操作系统使用虚拟寻址,处理器将其转换为物理地址,以及页面错误等。它们跟踪哪些页面属于哪些应用程序。但是,您根本不必担心这一点,因为您只需使用编程语言用来分配和释放内存的任何方法,并检查错误(如果分配/释放由于任何原因而失败)。

  • 什么决定了它们的大小?

    同样,它取决于语言,编译器,操作系统和体系结构。堆栈通常是预先分配的,因为根据定义,它必须是连续的内存。语言编译器或操作系统确定其大小。您不会在堆栈上存储大量数据,因此它将足够大,以至于永远不要完全使用它,除非发生不必要的无穷递归(因此,“堆栈溢出”)或其他异常的编程决策。

    堆是可以动态分配的任何事物的通用术语。根据您看待它的方式,它会不断变化的大小。在现代处理器和操作系统中,它的确切工作方式无论如何都是非常抽象的,因此您通常不必担心它的内在工作方式,除非(在允许它的语言中)您不得使用会您尚未分配或已释放的内存。

  • 是什么使速度更快?

    堆栈速度更快,因为所有可用内存始终是连续的。无需维护所有可用内存段的列表,只需一个指向堆栈当前顶部的指针即可。为此,编译器通常将此指针存储在特殊的快速寄存器中。而且,堆栈上的后续操作通常集中在内存的非常近的区域,这在非常低的级别上有利于通过处理器片上缓存进行优化。


20
戴维(David)我不同意那是一个很好的形象,还是“下推式堆叠”是说明这个概念的好术语。当您将某些内容添加到堆栈中时,堆栈中的其他内容不会被下推,而是保留在原处。
thomasrutter 2012年

8
这个答案包括一个很大的错误。静态变量未分配在堆栈上。请参阅我的答案[link] stackoverflow.com/a/13326916/1763801进行澄清。您将“自动”变量与“静态”变量等同,但它们根本不一样
davec 2012年

13
具体来说,您说“静态分配的局部变量”是在堆栈上分配的。实际上,它们是在数据段中分配的。在堆栈上仅分配自动分配的变量(包括大多数但不包括所有局部变量,以及通过值而不是通过引用传递的函数参数之类的东西)。
davec 2012年

9
我刚刚意识到您是对的-在C语言中,静态分配是它自己的独立事物,而不是非动态事物的术语。我已经修改了答案,谢谢。
thomasrutter 2012年

5
它不仅是C。Java,Pascal,Python和许多其他语言都有静态分配,自动分配和动态分配的概念。说“静态分配”在几乎所有地方都意味着同一件事。在任何语言中,静态分配都不意味着“非动态”。您想要术语“自动”分配所描述的内容(即堆栈中的内容)。
davec 2012年

727

(我将此答案从另一个或多或少是这个问题的重复的问题上移开了。)

您问题的答案是特定于实现的,并且可能会因编译器和处理器体系结构而异。但是,这是一个简化的说明。

  • 堆栈和堆都是从底层操作系统分配的内存区域(通常是虚拟内存,按需映射到物理内存)。
  • 在多线程环境中,每个线程将具有其自己的完全独立的堆栈,但它们将共享堆。并发访问必须在堆上进行控制,而在堆栈上则不可能。

  • 堆包含已用和可用块的链接列表。通过从空闲块之一创建一个合适的块,可以满足堆上的新分配(按newmalloc)。这需要更新堆上的块列表。该元信息在堆上块也存储在堆上经常在小范围只是在每个块的前面。
  • 随着堆的增长,通常将新块从低地址分配到高地址。因此,您可以将堆视为存储块的,随着分配的内存,其大小会增加。如果堆对于分配而言太小,则通常可以通过从底层操作系统获取更多内存来增加大小。
  • 分配和释放许多小块可能会使堆处于这样一种状态,即在已使用的块之间散布着许多小空闲块。分配大块的请求可能会失败,因为即使空闲块的组合大小可能足够大,也没有一个空闲块足以满足分配请求。这称为堆碎片
  • 当与空闲块相邻的已用块被重新分配时,新的空闲块可以与相邻的空闲块合并以创建更大的空闲块,从而有效地减少了堆的碎片。

堆

堆栈

  • 堆栈通常与CPU上一个名为堆栈指针的特殊寄存器紧密配合使用。最初,堆栈指针指向堆栈的顶部(堆栈中的最高地址)。
  • CPU具有用于入堆栈并将其从堆栈弹出的特殊指令。每次推送都会将值存储在堆栈指针的当前位置,并减少堆栈指针。甲弹出检索值所指向的堆栈指针,然后增加了堆栈指针(不通过这样的事实相混淆增加一个值到堆栈降低堆栈指针和去除的值增加了。请注意,堆栈增长到底部)。存储和检索的值是CPU寄存器的值。
  • 调用函数时,CPU使用特殊指令来推送当前指令指针,即在堆栈上执行的代码的地址。然后,CPU通过将指令指针设置为所调用函数的地址来跳转至该函数。稍后,当函数返回时,旧的指令指针会从堆栈中弹出,并在调用函数后立即在代码处恢复执行。
  • 输入函数后,将减少堆栈指针以在堆栈上为本地(自动)变量分配更多空间。如果函数具有一个局部32位变量,则在堆栈上预留4个字节。当函数返回时,将堆栈指针移回以释放分配的区域。
  • 如果函数具有参数,则在调用函数之前将它们压入堆栈。然后,函数中的代码可以从当前堆栈指针向上浏览堆栈以找到这些值。
  • 嵌套函数调用的工作方式就像一种魅力。每个新的调用将分配函数参数,局部变量的返回地址和空间,并且这些激活记录可以堆叠用于嵌套调用,并在函数返回时以正确的方式展开。
  • 由于堆栈是有限的内存块,因此您可能会通过调用过多的嵌套函数和/或为局部变量分配过多的空间而导致堆栈溢出。通常,用于堆栈的存储区的设置方式是,在堆栈底部(最低地址)以下进行写入操作将触发CPU陷阱或异常。然后,运行时可以捕获这种异常情况,并将其转换为某种堆栈溢出异常。

堆栈

可以在堆而不是堆栈上分配函数吗?

不可以,功能(即本地或自动变量)的激活记录分配在堆栈上,不仅用于存储这些变量,而且还用于跟踪嵌套的函数调用。

堆的管理方式实际上取决于运行时环境。C用途malloc和C ++用途new,但是许多其他语言具有垃圾回收。

但是,堆栈是与处理器体系结构紧密相关的更底层的功能。在没有足够空间的情况下增长堆并不难,因为可以在处理堆的库调用中实现堆。但是,通常无法堆叠堆栈,因为只有在为时已晚时才发现堆栈溢出。并关闭执行线程是唯一可行的选择。


35
@Martin-比更抽象的接受答案更好的答案/解释。显示用于函数调用的堆栈指针/寄存器的示例汇编程序将更具说明性。
Bikal Lem 2012年

3
每个引用类型都是值类型(int,string等)的组成。可以说,值类型存储在堆栈中的方式要比它们作为引用类型的一部分时如何工作。
Nps

15
在我看来,这个答案是最好的,因为它帮助我理解了return语句的真正含义,以及它与我时不时遇到的这个“ return address”之间的关系,以及将函数压入堆栈的含义,以及为什么将函数压入堆栈。好答案!
亚历克斯

3
这是我认为最好的,即为提的是堆/堆栈是实现特定的。其他的答案承担了大量的关于语言和环境/ OS的东西。+1
Qix-蒙尼卡(Monica)

2
您的意思是“该函数中的代码然后可以从当前堆栈指针向上浏览堆栈以找到这些值。” ?您能详细说明一下吗?
Koray Tugay,2015年

404

在下面的C#代码中

public void Method1()
{
    int i = 4;
    int y = 2;
    class1 cls1 = new class1();
}

这是管理内存的方式

堆栈上变量的图片

Local Variables只要函数调用进入堆栈,它就只需持续。堆用于变量,这些变量的寿命我们并不是很早就知道,但是我们希望它们能持续一段时间。在大多数语言中,至关重要的一点是,我们要在编译时知道要将变量存储在堆栈中的大小。

对象(随着我们更新它们的大小而变化)在堆上,因为我们在创建时不知道它们将持续多长时间。在许多语言中,都是对垃圾堆进行垃圾收集,以查找不再具有任何引用的对象(例如cls1对象)。

在Java中,大多数对象直接进入堆。在诸如C / C ++之类的语言中,当您不处理指针时,结构和类通常可以保留在堆栈中。

更多信息可以在这里找到:

堆栈和堆内存分配之间的区别«timmurphy.org

和这里:

在堆栈和堆上创建对象

本文是上述图片的来源:六个重要的.NET概念:堆栈,堆,值类型,引用类型,装箱和拆箱-CodeProject

但请注意,其中可能包含一些错误。


15
这是不正确的。i和cls不是“静态”变量。它们被称为“本地”或“自动”变量。这是一个非常重要的区别。参见[link] stackoverflow.com/a/13326916/1763801进行的澄清
davec 2012年

9
我不是说它们是静态变量。我说过int和cls1是静态。它们的内存是静态分配的,因此它们进入堆栈。这与需要动态内存分配的对象相反,该对象因此需要在堆上进行分配。
Snowcrash 2012年

12
我引用“静态项目...堆放”。这完全是错误的。静态项目进入数据段,自动项目进入堆栈。
davec

14
同样,写那个codeproject文章的人也不知道他在说什么。例如,他说“原始的需要静态类型的内存”,这是完全不正确的。没有什么能阻止您在堆中动态分配基元,只需编写诸如“ int array [] = new int [num]”和voila之类的东西,瞧,它们是在.NET中动态分配的。那只是几种不正确的情况之一。
davec

8
我编辑了您的文章,因为您在堆栈和堆中的内容上犯了严重的技术错误。
汤姆·莱斯

209

堆栈 调用函数时,该函数的参数以及其他一些开销会放在堆栈上。一些信息(例如返回目的地)也存储在此处。当您在函数内声明变量时,该变量也分配在堆栈上。

取消分配堆栈非常简单,因为您总是以相反的顺序进行分配。输入函数时会添加堆栈内容,退出它们时会删除相应的数据。这意味着除非您调用许多调用许多其他函数的函数(或创建递归解决方案),否则您通常会停留在堆栈的较小区域内。

该堆 的堆是你把你快速创建数据,其中的通用名称。如果您不知道程序将要创建多少个太空飞船,则很可能使用新的(或malloc或等效的)运算符来创建每个太空飞船。这种分配将持续一段时间,因此很可能我们将以与创建它们不同的顺序释放事物。

因此,堆要复杂得多,因为最终会出现一些未使用的内存区域与块相互交错的情况-内存碎片化了。寻找所需大小的可用内存是一个难题。这就是为什么应该避免使用堆的原因(尽管仍然经常使用堆)。

实现 堆栈和堆的实现通常取决于运行时/ OS。通常,性能至关重要的游戏和其他应用程序会创建自己的内存解决方案,该解决方案从堆中获取大量内存,然后在内部进行分配,以避免依赖OS来获取内存。

仅当您的内存使用量与常规使用情况大不相同时(例如,对于您在一个大型操作中加载一个关卡并可以在另一个大型操作中将所有内容丢掉的游戏),这才是实用的。

内存中的物理位置 这与您想像的无关紧要,因为一项称为“ 虚拟内存 ”的技术使您的程序认为您可以访问物理数据在其他地方(甚至在硬盘上!)的某个地址。随着调用树的不断深入,您为堆栈获取的地址按顺序递增。堆的地址是不可预测的(即特定于隐含的),并且坦率地说并不重要。


16
强烈建议避免使用堆。现代系统具有良好的堆管理器,现代动态语言广泛使用堆(无需程序员真正担心它)。我会说使用堆,但是有了手动分配器,别忘了释放!
格雷格(Greg Hewgill)

2
如果可以使用堆栈或堆,请使用堆栈。如果您不能使用堆栈,那么实际上别无选择。我用了很多,当然也使用了std :: vector或类似的东西。对于新手来说,您可以避免堆,因为堆栈非常简单!
汤姆·莱斯

如果您的语言没有实现垃圾回收,那么智能指针(围绕指针的部分分配的对象将对动态分配的内存块进行引用计数)与垃圾回收紧密相关,并且是一种安全地管理堆的好方法和无泄漏的方式。它们是在各种框架中实现的,但是对于您自己的程序也很难实现。

1
“这就是为什么应该避免使用堆的原因(尽管仍然经常使用它)。” 我不确定这实际上意味着什么,尤其是在许多高级语言中对内存进行不同的管理时。由于此问题被标记为与语言无关,因此我要说的是,此特定注释/行的位置不正确且不适用。
LintfordPickle

2
好点@JonnoHampson-当您提出一个有效的观点时,我会争辩说,如果您正在使用带有GC的“高级语言”,那么您可能根本就不在乎内存分配机制-因此就不必甚至关心堆栈和堆是什么。
汤姆·莱斯

194

为了澄清,此答案的信息不正确(托马斯在评论后将其答案固定,很酷:))。其他答案只是避免解释静态分配的含义。因此,我将在下面解释三种主要的分配形式,以及它们通常与堆,堆栈和数据段的关系。我还将在C / C ++和Python中显示一些示例,以帮助人们理解。

“静态”(又名静态分配)变量未分配在堆栈上。不要这么假设-许多人这样做只是因为“静态”听起来很像“堆栈”。它们实际上既不存在于堆栈中,也不存在于堆中。是所谓的数据段的一部分

但是,通常最好考虑“ 作用域 ”和“ 生存期 ”,而不是“堆栈”和“堆”。

范围是指代码的哪些部分可以访问变量。通常,我们认为本地范围(只能由当前函数访问)与全局范围(可以在任何地方访问)相比,尽管范围会变得更加复杂。

生存期是指在程序执行期间何时分配和释放变量。通常,我们认为静态分配(变量将在程序的整个过程中持续存在,从而使它在多个函数调用之间存储相同的信息很有用)与自动分配(变量仅在单个调用函数中持续存在,从而使其对存储仅在函数运行期间使用的信息,完成后可以将其丢弃)与动态分配(持续时间在运行时定义的变量,而不是静态或自动的编译时)相比。

尽管大多数编译器和解释器在使用堆栈,堆等方面都类似地实现了此行为,但是只要行为正确,编译器有时可能会破坏这些约定。例如,由于优化,即使大多数局部变量存在于堆栈中,局部变量也可能仅存在于寄存器中或被完全删除。正如一些评论中指出的那样,您可以自由地实现甚至不使用堆栈或堆,而是使用其他一些存储机制的编译器(很少使用,因为堆栈和堆对此非常有用)。

我将提供一些简单的带注释的C代码来说明所有这些。最好的学习方法是在调试器下运行程序并观察行为。如果您喜欢阅读python,请跳到答案的结尾:)

// Statically allocated in the data segment when the program/DLL is first loaded
// Deallocated when the program/DLL exits
// scope - can be accessed from anywhere in the code
int someGlobalVariable;

// Statically allocated in the data segment when the program is first loaded
// Deallocated when the program/DLL exits
// scope - can be accessed from anywhere in this particular code file
static int someStaticVariable;

// "someArgument" is allocated on the stack each time MyFunction is called
// "someArgument" is deallocated when MyFunction returns
// scope - can be accessed only within MyFunction()
void MyFunction(int someArgument) {

    // Statically allocated in the data segment when the program is first loaded
    // Deallocated when the program/DLL exits
    // scope - can be accessed only within MyFunction()
    static int someLocalStaticVariable;

    // Allocated on the stack each time MyFunction is called
    // Deallocated when MyFunction returns
    // scope - can be accessed only within MyFunction()
    int someLocalVariable;

    // A *pointer* is allocated on the stack each time MyFunction is called
    // This pointer is deallocated when MyFunction returns
    // scope - the pointer can be accessed only within MyFunction()
    int* someDynamicVariable;

    // This line causes space for an integer to be allocated in the heap
    // when this line is executed. Note this is not at the beginning of
    // the call to MyFunction(), like the automatic variables
    // scope - only code within MyFunction() can access this space
    // *through this particular variable*.
    // However, if you pass the address somewhere else, that code
    // can access it too
    someDynamicVariable = new int;


    // This line deallocates the space for the integer in the heap.
    // If we did not write it, the memory would be "leaked".
    // Note a fundamental difference between the stack and heap
    // the heap must be managed. The stack is managed for us.
    delete someDynamicVariable;

    // In other cases, instead of deallocating this heap space you
    // might store the address somewhere more permanent to use later.
    // Some languages even take care of deallocation for you... but
    // always it needs to be taken care of at runtime by some mechanism.

    // When the function returns, someArgument, someLocalVariable
    // and the pointer someDynamicVariable are deallocated.
    // The space pointed to by someDynamicVariable was already
    // deallocated prior to returning.
    return;
}

// Note that someGlobalVariable, someStaticVariable and
// someLocalStaticVariable continue to exist, and are not
// deallocated until the program exits.

为什么区分生命周期和作用域很重要的一个特别令人毛骨悚然的示例是变量可以具有局部作用域,但具有静态生命周期-例如,上面的代码示例中的“ someLocalStaticVariable”。这样的变量会使我们常见但非正式的命名习惯非常混乱。例如,当我们说“ 本地 ”时,我们通常是指“ 本地范围内的自动分配的变量 ”,而当我们说“ 全局 ”时,我们通常是指“ 全局范围的静态分配的变量 ”。不幸的是,当涉及到“ 文件范围内的静态分配的变量 ”之类的事情时,许多人只是说...“ 呵呵 ”。

C / C ++中的某些语法选择加剧了这个问题-例如,由于以下所示的语法,许多人认为全局变量不是“静态”的。

int var1; // Has global scope and static allocation
static int var2; // Has file scope and static allocation

int main() {return 0;}

请注意,在上面的声明中添加关键字“ static”可防止var2具有全局作用域。但是,全局var1具有静态分配。这不直观!因此,在描述范围时,我尽量不要使用“静态”一词,而应使用诸如“文件”或“文件受限”范围之类的名称。但是,许多人使用短语“静态”或“静态作用域”来描述只能从一个代码文件访问的变量。在生命周期的上下文中,“静态” 始终表示变量在程序启动时分配,并在程序退出时释放。

有些人认为这些概念是C / C ++特有的。他们不是。例如,下面的Python示例说明了所有三种分配类型(在解释性语言中可能存在一些细微的差异,我将不在这里介绍)。

from datetime import datetime

class Animal:
    _FavoriteFood = 'Undefined' # _FavoriteFood is statically allocated

    def PetAnimal(self):
        curTime = datetime.time(datetime.now()) # curTime is automatically allocatedion
        print("Thank you for petting me. But it's " + str(curTime) + ", you should feed me. My favorite food is " + self._FavoriteFood)

class Cat(Animal):
    _FavoriteFood = 'tuna' # Note since we override, Cat class has its own statically allocated _FavoriteFood variable, different from Animal's

class Dog(Animal):
    _FavoriteFood = 'steak' # Likewise, the Dog class gets its own static variable. Important to note - this one static variable is shared among all instances of Dog, hence it is not dynamic!


if __name__ == "__main__":
    whiskers = Cat() # Dynamically allocated
    fido = Dog() # Dynamically allocated
    rinTinTin = Dog() # Dynamically allocated

    whiskers.PetAnimal()
    fido.PetAnimal()
    rinTinTin.PetAnimal()

    Dog._FavoriteFood = 'milkbones'
    whiskers.PetAnimal()
    fido.PetAnimal()
    rinTinTin.PetAnimal()

# Output is:
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is tuna
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is steak
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is steak
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is tuna
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is milkbones
# Thank you for petting me. But it's 13:05:02.256000, you should feed me. My favorite food is milkbones

我将把在函数内声明的静态变量称为仅具有本地可访问性,但通常不将术语“作用域”与其一起使用。同样,可能值得注意的是,语言基本上具有零灵活性的一个堆栈/堆方面:将执行上下文保存在堆栈中的语言不能使用相同的堆栈来容纳需要在创建它们的上下文中生存的东西。 。某些语言(例如)PostScript具有多个堆栈,但行为却更像堆栈。
2013年

@supercat一切都说得通。我将范围定义为“代码的哪些部分可以访问变量”(并认为这是最标准的定义),所以我认为我们同意:)
davec 2013年

我认为变量的“范围” 受时间和空间的限制。只要对象存在,就需要在类对象范围内的变量保存其值。只要执行保留在该上下文中,就需要在执行上下文范围内的变量保存其值。静态变量声明创建一个标识符,该标识符的范围绑定到当前块,该标识符附加到其范围不受限制的变量
supercat

@supercat这就是为什么我使用寿命这个词的原因,这就是我所说的时间范围。它减少了使“作用域”一词具有多种含义的负担。据我所知,尽管确切的定义之间似乎还没有完全共识。我的术语部分来自K&R,部分来自我学习/授课的第一个CS部门的常用用法。总是很高兴听到另一种见解的观点。
davec

1
你一定在开玩笑。您真的可以在函数内部定义静态变量吗?
Zaeem Sattar

168

其他人已经很好地回答了大笔画,因此我将介绍一些细节。

  1. 堆栈和堆不必是单数。拥有多个堆栈的一种常见情况是,如果一个进程中有多个线程。在这种情况下,每个线程都有自己的堆栈。您也可以有多个堆,例如某些DLL配置可能导致从不同堆分配不同的DLL,这就是为什么释放由不同库分配的内存通常不是一个好主意的原因。

  2. 在C语言中,可以通过使用alloca来获得可变长度分配的好处,alloca可以在堆栈上进行分配,而alloc则可以在堆上进行分配。此内存将无法在return语句中保留下来,但是对于暂存缓冲区很有用。

  3. 在Windows上使用不多的巨大临时缓冲区并不是免费的。这是因为编译器将生成一个每次进入函数以确保堆栈存在时都会调用的堆栈探测循环(因为Windows在堆栈末尾使用单个保护页来检测何时需要增加堆栈。如果您访问的内存超出了堆栈末尾的一页,则会崩溃。例:

void myfunction()
{
   char big[10000000];
   // Do something that only uses for first 1K of big 99% of the time.
}

关于“与alloc相对”:您的意思是“与malloc相对”吗?
Peter Mortensen

有多便携alloca
Peter Mortensen

@PeterMortensen不是POSIX,不能保证可移植性。
唐·诺伊菲尔德

135

其他人直接回答了您的问题,但是当试图了解堆栈和堆时,我认为考虑传统UNIX进程(没有线程和mmap()基于分配器)的内存布局会有所帮助。该内存管理词汇网页都有此内存布局的示意图。

传统上,堆栈和堆位于进程的虚拟地址空间的相对两端。堆栈在访问时会自动增长,直至达到内核设置的大小(可以使用进行调整setrlimit(RLIMIT_STACK, ...))。当内存分配器调用brk()sbrk()系统调用时,堆会增长,从而将更多的物理内存页面映射到进程的虚拟地址空间。

在没有虚拟内存的系统(例如某些嵌入式系统)中,通常使用相同的基本布局,只是堆栈和堆的大小固定不变。但是,在其他嵌入式系统(例如基于Microchip PIC微控制器的嵌入式系统)中,程序堆栈是单独的存储器块,数据移动指令无法对其进行寻址,并且只能通过程序流指令(调用,返回等)。其他体系结构(例如Intel Itanium处理器)具有多个堆栈。从这个意义上讲,堆栈是CPU体系结构的元素。


117

什么是堆栈?

堆栈是一堆物体,通常是整齐排列的物体。

在此处输入图片说明

计算体系结构中的堆栈是内存中以先进先出方式添加或删除数据的区域。
在多线程应用程序中,每个线程将具有自己的堆栈。

什么是堆?

堆是随意堆放的东西的不整洁集合。

在此处输入图片说明

在计算体系结构中,堆是由操作系统或内存管理器库自动管理的动态分配内存区域。
在程序执行期间定期对堆上的内存进行分配,释放和调整大小,这可能导致称为碎片的问题。
当在内存对象之间分配的小空间太小而无法容纳其他内存对象时,就会发生碎片。
最终结果是无法用于进一步的内存分配的堆空间的百分比。

两者一起

在多线程应用程序中,每个线程将具有自己的堆栈。但是,所有不同的线程将共享堆。
由于不同的线程在多线程应用程序中共享堆,因此这也意味着线程之间必须进行某种协调,以便它们不会尝试访问和操作堆中相同的内存。同一时间。

哪个更快-堆栈还是堆?又为什么呢

堆栈比堆快得多。
这是因为在堆栈上分配内存的方式。
在堆栈上分配内存就像向上移动堆栈指针一样简单。

对于刚接触编程的人来说,使用堆栈可能是个好主意,因为它比较容易。
因为堆栈很小,所以当您确切知道数据将需要多少内存时,或者如果您知道数据的大小很小时,您可能希望使用它。
当您知道您的数据将需要大量内存,或者不确定是否需要多少内存(例如动态数组)时,最好使用堆。

Java内存模型

在此处输入图片说明

堆栈是存储本地变量(包括方法参数)的内存区域。当涉及对象变量时,这些仅仅是对堆上实际对象的引用(指针)。
每次实例化一个对象时,都会将一大堆堆内存放在一边以保存该对象的数据(状态)。由于对象可以包含其他对象,因此某些数据实际上可以保存对这些嵌套对象的引用。


115

堆栈是内存的一部分,可以通过一些关键的汇编语言指令进行操作,例如“ pop”(从堆栈中删除并返回值)和“ push”(将值推入堆栈),但也可以调用(调用子例程-将地址压入堆栈以返回堆栈)并返回(从子例程返回-将地址从堆栈中弹出并跳转至堆栈)。它是堆栈指针寄存器下方的内存区域,可以根据需要进行设置。堆栈还用于将参数传递给子例程,还用于在调用子例程之前将值保留在寄存器中。

堆是操作系统通常通过诸如malloc之类的syscall分配给应用程序的一部分内存。在现代OS上,此内存是只有调用进程才能访问的一组页面。

堆栈的大小是在运行时确定的,通常在程序启动后不会增长。在C程序中,堆栈必须足够大以容纳每个函数中声明的每个变量。堆将根据需要动态增长,但是操作系统最终会进行调用(它通常会使堆增长超过malloc请求的值,因此至少将来的某些malloc不需要返回内核来完成)。获得更多内存。此行为通常可以自定义)

因为在启动程序之前已经分配了堆栈,所以您不需要使用malloc就可以使用堆栈,因此那是一个小优势。在实践中,很难预测在具有虚拟内存子系统的现代操作系统中哪些将是快的,哪些将是慢的,因为页面的实现方式和存储位置是实现细节。


2
在这里还值得一提的是intel极大地优化了堆栈访问,特别是诸如预测从函数返回的位置之类的事情。
汤姆·莱斯

113

我认为许多其他人在这个问题上给了您大多数正确的答案。

但是,遗漏的一个细节是“堆”实际上应该称为“免费存储”。区别的原因是原始的免费存储区是通过称为“二项式堆”的数据结构实现的。因此,从早期实现的malloc()/ free()进行分配是从堆进行分配。但是,在当今的今天,大多数免费存储都是用非常复杂的数据结构实现的,这些数据结构不是二项式堆。


8
另一个挑剔的答案(大部分)(暗示)暗示该C语言需要使用“堆栈” 。这是一个普遍的误解,尽管它是(到目前为止)实现C99 6.2.4 automatic storage duration objects(变量)的主要范例。实际上,“ stack”一词甚至没有出现在C99语言标准中:open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
johne

[@Heath]对于您的回答,我有一点评论。看一下这个问题的公认答案。它说免费存储 很可能相同,尽管不一定如此。
OmarOthman '02

91

您可以使用堆栈做一些有趣的事情。例如,您具有诸如alloca之类的功能(假设您可以克服有关其使用的大量警告),这是一种malloc的形式,专门使用堆栈而不是堆作为内存。

也就是说,基于堆栈的内存错误是我遇到的最严重的错误。如果使用堆内存,并且超出了分配的块的边界,则触发分段错误的机会就很大。(不是100%:您的块可能偶然与您先前分配的另一个相邻。)但是,由于在堆栈上创建的变量始终彼此相邻,因此越界写入可以更改另一个变量的值。我了解到,每当我感到程序停止遵守逻辑规则时,它就有可能是缓冲区溢出。


有多便携alloca?例如,它可以在Windows上运行吗?它仅适用于类似Unix的操作系统吗?
Peter Mortensen

89

简单来说,堆栈是在其中创建局部变量的地方。同样,每次调用子例程时,程序计数器(指向下一条机器指令的指针)和任何重要的寄存器,有时参数都被压入堆栈。然后,子例程中的所有局部变量都被压入堆栈(并从那里使用)。子例程完成后,所有东西将从堆栈中弹出。PC和注册数据将被获取并放回弹出位置,因此程序可以继续进行。

堆是进行动态内存分配的内存区域(显式的“ new”或“ allocate”调用)。它是一种特殊的数据结构,可以跟踪大小不同的内存块及其分配状态。

在“经典”系统中,RAM的布局使得堆栈指针从内存的底部开始,堆指针从内存的顶部开始,并且它们相互靠近。如果它们重叠,则说明内存不足。但是,这不适用于现代多线程操作系统。每个线程都必须具有自己的堆栈,并且可以动态创建这些堆栈。


[@TED]您为什么说“有时参数被压入堆栈”?我知道的是,他们一直都是。您能详细说明一下吗?
奥马尔·奥斯曼(OmarOthman)2012年

1
@OmarOthman-我说这是因为,完全取决于您的编译器/解释器的编写者,当调用子例程时会发生什么。经典的Fortran行为是根本不使用堆栈。某些语言支持诸如通名之类的奇特事物,实际上是一种文本替代。
TED 2012年

83

来自WikiAnwser。

当一个函数或方法调用另一个函数,依次调用另一个函数等时,所有这些函数的执行将保持挂起状态,直到最后一个函数返回其值为止。

挂起的函数调用链是堆栈,因为堆栈中的元素(函数调用)相互依赖。

在异常处理和线程执行中,必须考虑堆栈。

堆只是程序用于存储变量的内存。堆的元素(变量)彼此之间没有依赖性,并且始终可以随时随地进行随机访问。


“我更喜欢被接受的答案,因为它的水平更低。” 那是坏事,不是好事。
Lightness Races in Orbit

54

  • 快速访问
  • 不必显式取消分配变量
  • CPU有效管理空间,内存不会碎片化
  • 仅局部变量
  • 堆栈大小限制(取决于OS)
  • 变量无法调整大小

  • 变量可以全局访问
  • 内存大小无限制
  • (相对)访问速度较慢
  • 无法保证有效利用空间,随着时间的流逝,随着分配内存块然后释放内存,内存可能会碎片化
  • 您必须管理内存(您负责分配和释放变量)
  • 可以使用realloc()调整变量的大小

50

好吧,简单地说,它们的意思是有序的不是有序的 ……!

堆放:在堆放物品中,东西相互叠放,这意味着将更快,更高效地进行处理!...

因此总会有一个索引来指向特定的项目,并且处理也会更快,项目之间也有关系!...

:没有顺序,处理会变慢,并且值被弄乱了,没有特定的顺序或索引...是随机的,它们之间没有关系...因此执行和使用时间可能会有所不同...

我还创建了下面的图像,以显示它们的外观:

在此处输入图片说明


49

简而言之

堆栈用于存储静态内存,堆用于存储动态内存,两者均存储在计算机的RAM中。


详细

堆栈

堆栈是一个“ LIFO”(后进先出)数据结构,该结构由CPU非常紧密地管理和优化。每当函数声明一个新变量时,它都会“推送”到堆栈中。然后,每次函数退出时,该函数压入堆栈的所有变量都将被释放(也就是说,它们将被删除)。释放堆栈变量后,该内存区域可用于其他堆栈变量。

使用堆栈存储变量的优点是可以为您管理内存。您不必手动分配内存,也可以在不再需要时释放它。此外,由于CPU如此高效地组织堆栈存储器,因此读写堆栈变量的速度非常快。

这里可以找到更多。


堆是计算机内存中不会自动为您管理的区域,并且不受CPU严格管理。它是内存中更自由浮动的区域(并且更大)。要在堆上分配内存,必须使用内置的C函数malloc()或calloc()。在堆上分配内存后,一旦不再需要使用free()负责分配该内存。

如果不这样做,程序将发生所谓的内存泄漏。也就是说,堆上的内存仍将被保留(并且其他进程将无法使用)。正如我们将在调试部分看到的那样,有一个名为Valgrind的工具可以帮助您检测内存泄漏。

与堆栈不同,堆对可变大小没有大小限制(除了计算机的明显物理限制之外)。堆内存的读取和写入速度稍慢,因为必须使用指针来访问堆上的内存。我们将在短期内讨论指针。

与堆栈不同,在堆上创建的变量可以由程序中任何位置的任何函数访问。堆变量的范围本质上是全局的。

这里可以找到更多。


在堆栈上分配的变量直接存储到内存中,对该内存的访问非常快,并且在编译程序时会处理其分配。当一个函数或方法调用另一个函数,然后又调用另一个函数等时,所有这些函数的执行将保持挂起状态,直到最后一个函数返回其值为止。堆栈始终按LIFO顺序保留,最近保留的块始终是要释放的下一个块。这使得跟踪堆栈真的非常简单,从堆栈中释放一个块只不过是调整一个指针而已。

在堆上分配的变量在运行时分配了内存,访问该内存的速度稍慢,但是堆大小仅受虚拟内存大小的限制。堆中的元素彼此之间没有依赖关系,并且始终可以随时随地进行随机访问。您可以随时分配一个块,并随时释放它。这使跟踪在任何给定时间分配或释放堆的哪些部分变得更加复杂。

在此处输入图片说明

如果您确切知道在编译之前需要分配多少数据,并且它不会太大,则可以使用堆栈。如果您不确切知道运行时将需要多少数据,或者是否需要分配大量数据,则可以使用堆。

在多线程情况下,每个线程将具有其自己的完全独立的堆栈,但它们将共享堆。堆栈是特定于线程的,堆是特定于应用程序的。在异常处理和线程执行中,必须考虑堆栈。

每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管对于不同类型的分配有多个堆并不少见)。

在此处输入图片说明

在运行时,如果应用程序需要更多的堆,则可以从可用内存中分配内存,如果堆栈需要内存,则可以从可用内存中为应用程序分配的内存中分配内存。

甚至在这里这里都给出更多细节。


现在来回答您的问题

它们在多大程度上受操作系统或语言运行时的控制?

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

这里可以找到更多。

他们的范围是什么?

已经在顶部。

“如果您确切地知道在编译之前需要分配多少数据,并且它不会太大,那么可以使用堆栈。如果您不确切知道在运行时需要多少数据,或者您需要分配大量数据。”

这里可以找到更多。

什么决定了它们的大小?

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

是什么使速度更快?

堆栈分配要快得多,因为它真正要做的只是移动堆栈指针。使用内存池,您可以从堆分配中获得可比的性能,但这会带来一点点复杂性和麻烦。

而且,堆栈与堆不仅是性能方面的考虑;它还告诉您很多有关对象的预期寿命的信息。

可以从这里找到详细信息。


36

在1980年代,UNIX像小兔子一样传播,大公司也纷纷成立自己的公司。埃克森美孚(Exxon)和一个失去历史的品牌一样拥有一个。内存的布局方式由许多实现者决定。

典型的C程序在内存中平面放置,可以通过更改brk()值来增加。通常,HEAP刚好低于此brk值,并且增加brk会增加可用堆的数量。

单个STACK通常位于HEAP之下的区域,该区域是直到下一固定内存块顶部之前都没有值的内存区域。下一个块通常是CODE,在那个时代著名的黑客之一中,它可能会被堆栈数据覆盖。

一个典型的存储块是BSS(零值块),它在一个制造商的产品中偶然没有被置零。另一个是包含初始化值的DATA,包括字符串和数字。第三个是包含CRT(C运行时),主要,功能和库的CODE。

UNIX中虚拟内存的出现改变了许多限制。没有客观的原因说明为什么这些块需要连续,大小固定或现在以特定方式订购。当然,在UNIX出现之前,Multics不受这些限制。这是显示那个时代的一种存储器布局的示意图。

典型的1980年代风格的UNIX C程序内存布局



26

几分钱:我认为,以图形方式绘制内存并且更简单会很好:

这是我对进程内存构造进行简化的愿景,以便更轻松地了解正在发生的事情


箭头-通常在线程创建API中通过参数显示增长堆栈和堆的位置,进程堆栈大小的限制(在OS中定义),线程堆栈大小的限制。堆通常受进程最大虚拟内存大小的限制,例如32位2-4 GB。

如此简单的方式:进程堆是进程及其内部所有线程的通用对象,通常在诸如malloc()之类的情况下用于内存分配。

堆栈是快速存储器,用于在通常情况下存储函数返回指针和变量,并作为函数调用中的参数(局部函数变量)进行处理。


23

由于有些答案很挑剔,所以我将贡献自己的一份力量。

令人惊讶的是,没有人提到,不仅在异类语言(PostScript)或平台(Intel Itanium)中,而且在光纤绿色线程中都可以找到多个(即,与正在运行的OS级线程的数量无关)调用堆栈。和协程的一些实现。

纤维,生丝和协程在许多方面都相似,这导致很多混乱。光纤和绿色线程之间的区别在于,前者使用协作式多任务处理,而后者则可能采用协作式或抢占式(或什至两者)。有关纤维和协程之间的区别,请参见此处

无论如何,光纤,绿色线程和协程的目的是在一个OS级线程中同时执行多个功能,但不能并行执行(有关区别,请参见此SO问题),从而相互之间来回传递控制以有组织的方式

使用纤维,绿线或协程时, 通常每个功能都有单独的堆栈。(从技术上讲,每个函数不仅是堆栈,而且整个执行上下文是每个函数。最重要的是,CPU寄存器。)对于每个线程,堆栈与同时运行的函数一样多,并且线程在执行每个函数之间切换根据您程序的逻辑。当函数运行到末尾时,其堆栈将被破坏。因此,堆栈的数量和生存期是动态的,而不是由OS级线程的数量决定的!

请注意,我说的是“ 每个函数通常有单独的堆栈”。有俩都stackful无堆叠 couroutines的实现。最著名的堆栈式C ++实现是Boost.CoroutineMicrosoft PPLasync/await。(但是,C ++ 17提出的C ++ 可恢复函数(又名“ asyncawait”)很可能使用无堆栈协程。)

即将向C ++标准库提出光纤建议。另外,还有一些第三方。绿色线程在Python和Ruby等语言中非常流行。


19

我已经分享了一些要点,尽管要点已经涵盖了。

  • 快速访问。
  • 存储在RAM中。
  • 函数调用与传递的局部变量和函数参数一起加载到此处。
  • 当程序超出范围时,将自动释放空间。
  • 存储在顺序存储器中。

  • 相对较慢地访问堆栈。
  • 存储在RAM中。
  • 动态创建的变量存储在此处,以后需要使用后释放分配的内存。
  • 存储在完成内存分配的任何位置,始终由指针访问。

有趣的注意事项:

  • 如果将函数调用存储在堆中,则会导致2个混乱点:
    1. 由于堆栈中的顺序存储,因此执行速度更快。堆中的存储会导致大量时间消耗,从而使整个程序的执行速度变慢。
    2. 如果函数存储在堆中(指针指向的混乱存储),则将无法返回到调用者地址(由于内存中的顺序存储,因此堆栈可以返回)。

1
简洁明了。不错:)
ingconti

13

哇!如此众多的答案,我认为其中没有一个是正确的...

1)它们在哪里和什么(物理上在真实计算机的内存中)?

堆栈是从分配给程序映像的最高内存地址开始的内存,然后从那里递减值。它保留给调用的函数参数以及函数中使用的所有临时变量。

有两个堆:公共堆和私有堆。

私有堆从程序中代码的最后一个字节之后的16字节边界(对于64位程序)或8字节边界(对于32位程序)开始,然后从那里开始增加值。也称为默认堆。

如果私有堆太大,则它将与堆栈区域重叠,如果私有堆太大,则堆栈将与堆栈重叠。由于堆栈从较高的地址开始,然后一直向下移动至较低的地址,因此通过适当的破解,您可以使堆栈变大,以至于溢出专用堆区域并重叠代码区域。然后,诀窍是要重叠足够的代码区域,以便可以将其连接到代码中。这样做有些棘手,您可能会遇到程序崩溃的风险,但它既简单又有效。

公共堆位于程序映像空间之外的自己的内存空间中。如果内存资源稀缺,则会将该内存虹吸到硬盘上。

2)它们在多大程度上受操作系统或语言运行时的控制?

堆栈由程序员控制,私有堆由OS管理,而公共堆则不受任何人控制,因为它是OS服务-您发出请求,然后授予或拒绝请求。

2b)他们的范围是什么?

它们对于该程序都是全局的,但是其内容可以是私有的,公共的或全局的。

2c)是什么决定了每个尺寸?

堆栈和专用堆的大小由编译器运行时选项确定。公共堆在运行时使用size参数初始化。

2d)是什么使速度更快?

它们并不是为了快速而设计的,而是被设计为有用的。程序员如何利用它们确定它们是“快速”还是“慢速”

参考:

https://norasandler.com/2019/02/18/Write-a-Compiler-10.html

https://docs.microsoft.com/zh-CN/windows/desktop/api/heapapi/nf-heapapi-getprocessheap

https://docs.microsoft.com/zh-CN/windows/desktop/api/heapapi/nf-heapapi-heapcreate


8

许多答案在概念上都是正确的,但我们必须注意,硬件(即微处理器)需要一个堆栈才能允许调用子例程(汇编语言中的CALL ..)。(OOP家伙会称其为方法

在堆栈上,您保存返回地址并调用→推入/退出→弹出直接在硬件中进行管理。

您可以使用堆栈来传递参数..即使它比使用寄存器要慢(微处理器专家或一本好的1980年代BIOS书籍也会这样)。

  • 没有堆没有微处理器可以工作。(我们无法想象没有子例程/函数的程序,即使是汇编语言)
  • 没有堆就可以。(汇编语言程序可以不工作,因为堆是OS概念,而malloc则是OS / Lib调用。

堆栈使用速度更快,因为:

  • 是硬件,甚至push / pop都非常有效。
  • malloc需要进入内核模式,使用锁/信号量(或其他同步原语)执行一些代码,并管理一些跟踪分配所需的结构。

什么是OPP?您的意思是OOP(面向对象编程)吗?
彼得·莫滕森

您的意思是说这malloc是内核调用吗?
Peter Mortensen

1)是的,对不起.. OOP ... 2)malloc:我写了一下,对不起... malloc在用户空间中..但是可以触发其他调用....问题是使用堆可能很慢...
ingconti

许多答案作为概念都是正确的,但是我们必须注意,硬件(即微处理器)需要一个堆栈来允许调用子例程(汇编语言中的CALL。 ”)。您会混淆CPU堆栈(如果现代CPU中只有一个)和语言运行时堆栈(每个线程一个)。当程序员谈论堆栈时,这就是运行时的线程执行堆栈,例如NET线程堆栈,而我们并不是在谈论CPU堆栈。
分钟

1

堆栈本质上是一个易于访问的内存,可以简单地将其项目作为堆栈进行管理。只有事先知道尺寸的物品才能进入堆栈。数字,字符串,布尔值就是这种情况。

堆是其中的你不能预先确定的确切大小和结构的物品存储。由于对象和数组可以在运行时进行更改和更改,因此它们必须进入堆中。

资料来源:Academind


0

谢谢您进行了很好的讨论,但是作为一个真正的菜鸟,我想知道将说明保存在哪里?在开始阶段,科学家们在两种体系结构之间进行决定(冯·纽曼(von NEUMANN)将所有内容都视为数据,而哈佛(HARVARD)则将内存区域用于指令,将另一区域用于数据)。最终,我们采用了冯·诺依曼(von Neumann)的设计,现在一切都被认为是“相同的”。当我学习汇编https://www.cs.virginia.edu/~evans/cs216/guides/x86.html时,这对我来说很困难, 因为它们谈论寄存器和堆栈指针。

上面的所有内容都谈到了DATA。我的猜测是,由于指令是具有特定内存占用空间的已定义事物,因此它将进入堆栈,因此汇编中讨论的所有“那些”寄存器都位于堆栈中。当然,然后是面向对象的程序设计,其中指令和数据混合成一个动态的结构,所以现在指令也将保留在堆中吗?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.