栈和堆是什么?它们分别位于哪里?

9349
  • 栈和堆是什么?
  • 它们在计算机内存中的物理位置在哪里?
  • 它们在多大程度上受操作系统或语言运行时的控制?
  • 它们的作用范围是什么?
  • 是什么决定它们的大小?
  • 是什么使它们更快?

246
这里有一个非常好的解释,可以阐述栈和堆之间的区别:什么是栈和堆?它们有何区别? - Songo
18
也很好:http://www.codeproject.com/Articles/76153/Six-important-NET-concepts-Stack-heap-value-types(关于堆栈/堆的部分) - Ben
4
请参见 Stack Clash。栈冲突修复措施会影响诸如 rlimit_stack 等系统变量和行为的某些方面。另请参见 Red Hat 的 Issue 1463241 - jww
4
栈和堆的定义与值类型和引用类型无关,即使没有值类型和引用类型,栈和堆也可以完全定义。而且,在理解值类型和引用类型时,栈只是一种实现细节。根据Eric Lippert的说法:栈是一种实现细节(第一部分) - Matthew
显示剩余2条评论
32个回答

6774
没有一个固定的因素决定哪一个更快,这取决于具体的使用情况。通常来说,栈操作比堆操作更快,因为它们使用简单的指针调整来分配和释放内存,而不需要像堆一样进行复杂的管理。此外,在访问堆中已分配的内存时需要使用指针,而这可能会带来额外的开销。然而,对于动态分配和释放内存的情况,堆是必需的,并且在某些情况下,堆甚至可以比栈更快。栈更快,因为访问模式使得从中分配和释放内存变得非常简单(指针/整数仅需递增或递减),而堆涉及更复杂的分配或释放操作。此外,栈中的每个字节往往被频繁重用,这意味着它往往被映射到处理器的缓存中,从而非常快速。堆的另一个性能问题是,堆大多是全局资源,通常需要支持多线程,即程序中的每次分配和释放都需要与“所有”其他堆访问同步。
一种明显的示例:
图片来源:vikashazrati.wordpress.com

139
好的回答,但我认为你应该补充一下,虽然在进程启动时(假设有操作系统存在)堆栈是由操作系统分配的,但程序内联维护了堆栈。这也是堆栈更快的另一个原因 - 压入和弹出操作通常只需要一条机器指令,现代计算机可以在一个周期内执行至少3个这样的指令,而分配或释放堆则涉及调用操作系统代码。 - sqykly
479
我对最后的图表感到非常困惑。在看到那张图片之前,我认为自己理解了它。 - Sina Madani
16
处理器能够在有或没有操作系统的情况下运行指令。一个我非常了解的例子是SNES,它没有API调用,也没有像今天我们所知道的操作系统,但它有一个堆栈。在这些系统上进行堆栈分配就是加减操作,对于那些在函数返回时被弹出并销毁的变量来说这是可以的,但是相比之下,像构造函数这样的情况就不行了,因为它们的结果不能被丢弃。为此,我们需要堆,它不依赖于调用和返回。大多数操作系统都有APIs和堆,所以没有必要自己实现。 - sqykly
8
"stack被作为临时空间保留的内存。"挺酷的,但是在Java内存结构中它实际上是在哪里“保留”的?是在堆内存/非堆内存/其他地方(参考 https://betsol.com/2017/06/java-memory-management-for-java-virtual-machine-jvm/ 中的Java内存结构)?" - chepaiytrath
8
Java运行时作为字节码解释器,增加了一个虚拟化层级,因此你所提到的只是Java应用程序的观点。从操作系统的角度来看,这只是一个堆,Java运行时进程分配一些空间作为“非堆”内存来处理字节码。剩下的操作系统级别的堆被用作应用程序级别的堆,用于存储对象数据。 - kbec
显示剩余9条评论

2676

堆栈:

  • 与堆一样存储在计算机的RAM中。
  • 在堆栈上创建的变量会在超出作用域时自动释放。
  • 与堆上的变量相比,分配速度更快。
  • 使用实际的堆栈数据结构实现。
  • 存储本地数据、返回地址,用于参数传递。
  • 可能会因为使用了太多的堆栈空间(主要是由于无限或太深的递归,非常大的分配)而导致堆栈溢出。
  • 堆栈上创建的数据可以在不使用指针的情况下使用。
  • 如果你知道在编译时需要分配多少数据并且这个数据不太大,那么你可以使用堆栈。
  • 通常在程序启动时就已经确定了其最大大小。

堆:

  • 与堆栈一样存储在计算机的RAM中。
  • 在C++中,堆上的变量必须手动销毁,不能超出作用域。数据通过deletedelete[]free来释放。
  • 与堆栈上的变量相比,分配速度较慢。
  • 按需使用,为程序分配一块数据块。
  • 当存在大量的分配和释放时可能会发生碎片。
  • 在C++或C中,堆上创建的数据将由指针指向,并使用newmalloc来分配。
  • 如果请求分配的缓冲区太大,则可能会出现分配失败。
  • 如果您不知道运行时需要多少数据,或者需要分配大量数据,则可以使用堆。
  • 负责内存泄漏。

示例:

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;

39
指针pBuffer和变量b存储在栈中,很可能是在函数入口处分配的。根据编译器的不同,缓冲区也可能在函数入口处分配。 - Andy
47
有一个普遍的误解,认为通过C99语言标准(网址为http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf)定义的 C 语言需要一个“堆栈”。事实上,在标准中甚至没有出现过“堆栈”这个词。总体上而言,关于 C 的堆栈使用情况的说法是正确的,但这并不是该语言必须要求的。请参阅http://www.knosof.co.uk/cbook/cbook.html了解更多信息,特别是有关如何在奇怪的体系结构上实现 C ,例如http://en.wikipedia.org/wiki/Burroughs_large_systems。 - johne
63
@Brian,您应该解释为什么需要在堆栈上创建buffer[]和pBuffer指针,以及为什么pBuffer的数据需要在堆上创建。我认为有些人可能会对您的答案感到困惑,因为他们可能会认为程序是明确指示在堆栈或堆上分配内存,但实际情况并非如此。这是因为Buffer是值类型,而pBuffer是引用类型吗? - Howiecamp
52
"负责处理内存泄漏" - 堆并不负责内存泄漏!懒惰、健忘或不在意的程序员才是原因! - Laz
8
关于作用域和分配的评论是错误的 - 作用域根本与堆栈或堆没有关联。在堆上的变量必须手动销毁,并且永远不会超出范围,这是不正确的;更准确的说法是“当引用它们的变量超出作用域时,堆上的数据不会被释放,需要由您(或垃圾回收器)来释放它们。” - Orion Edwards
显示剩余9条评论

1507
最重要的一点是堆和栈是分配内存的通用术语。它们可以以许多不同的方式实现,这些术语适用于基本概念。
在一个项的堆栈中,项按照放置的顺序一个接一个地叠放在顶部,您只能移除顶部的一个(而不会倒掉整个东西)。
堆栈的简单之处在于,您不需要维护包含每个分配内存部分记录的表;您所需的唯一状态信息是指向堆栈末尾的单个指针。要分配和取消分配,只需增加和减少该单个指针。注意:有时可以将堆栈实现为从内存段顶部开始并向下扩展而不是向上增长。
在堆中,没有特定的顺序来放置项目。您可以随意取出项目,因为没有明显的“顶部”项目。
堆分配需要维护完整的记录,说明哪些内存已分配,哪些未分配,以及一些开销维护以减少碎片化,查找足够大以适应请求大小的连续内存段等等。内存可以随时被释放,留下空闲空间。有时,内存分配器将执行维护任务,例如通过移动已分配的内存进行碎片整理或垃圾回收-在运行时识别内存何时不再在作用域内,并将其取消分配。

这些图片应该可以很好地描述在栈和堆中分配和释放内存的两种方式。美味!

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

    如前所述,堆和栈是通用术语,可以以许多方式实现。计算机程序通常有一个称为调用栈的栈,其中存储与当前函数相关的信息,例如指向调用它的任何函数的指针和任何本地变量。由于函数调用其他函数然后返回,因此堆栈会增长和缩小以容纳来自调用堆栈下方的函数的信息。程序并没有真正的运行时控制权;它由编程语言、操作系统甚至系统架构决定。

    堆是一个通用术语,用于分配动态和随机的任何内存;即无序的。通常由操作系统分配内存,应用程序调用API函数来执行此分配。管理动态分配的内存需要相当多的开销,通常由所使用的编程语言或环境的运行时代码处理。

  • 它们的作用域是什么?

    调用栈是一个如此低级别的概念,它不涉及到编程中“作用域”的意义。如果你反汇编一些代码,你会看到相对指针样式引用堆栈的部分,但就高级语言而言,语言会强制其自己的作用域规则。然而,堆栈的一个重要方面是,一旦函数返回,任何局部变量都会立即从堆栈中释放。这符合你在编程语言工作时所期望的方式。在堆中,它也很难定义。作用域是由操作系统公开的内容,但你的编程语言可能会为应用程序添加其规则。处理器架构和操作系统使用虚拟寻址,处理器将其转换为物理地址,并存在页面错误等问题。他们跟踪哪些页面属于哪个应用程序。不过,你通常不需要担心这个,因为你只需使用编程语言用于分配和释放内存的任何方法,并检查错误(如果分配/释放由于任何原因失败)。

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

    同样,这取决于语言、编译器、操作系统和体系结构。栈通常是预先分配的,因为根据定义,它必须是连续的内存。语言编译器或操作系统确定其大小。你不会在堆栈上存储大块数据,因此它的大小足够大,应该永远不会被完全使用,除非出现不想要的无限递归(因此,“堆栈溢出”)或其他不寻常的编程决策。

    堆是一个通用术语,用于动态分配任何东西。从某种意义上说,它的大小在不断变化。在现代处理器和操作系统中,它的确切工作方式非常抽象,因此通常不需要过多地担心其深层次的工作原理,除非你使用的编程语言允许你使用未分配的内存或已释放的内存。

  • 什么使其中一个更快?

    栈更快,因为所有空闲内存总是连续的。无需维护所有空闲内存段的列表,只需维护当前栈顶


27
David,我不同意那是一个好的形象或“push-down stack”是一个好的术语来说明这个概念。当你向堆栈中添加东西时,堆栈的其他内容并没有被推下去,它们仍然在原地。 - thomasrutter
16
这篇回答包含一个严重错误。静态变量不是在栈上分配的。请参考我的回答[链接] https://dev59.com/hHVD5IYBdhLWcg3wHn2d#13326916 进行澄清。你把“自动”变量和“静态”变量等同起来,但它们并不相同。 - davec
19
具体而言,你说“静态分配的本地变量”是在栈上分配的。实际上,它们是在数据段中分配的。只有自动分配的变量(其中包括大多数但不是所有本地变量以及像按值传递而不是按引用传递的函数参数这样的东西)才会在栈上分配。 - davec
15
我刚意识到你是对的 - 在C语言中,“静态分配”是一个独立的概念,而不仅仅是与“动态分配”相反的术语。我已经编辑了我的回答,谢谢。 - thomasrutter
8
不仅C语言,Java、Pascal、Python以及其他许多语言都具有静态、自动和动态分配的概念。在任何一种语言中,说“静态分配”几乎意味着相同的事情。在任何语言中,静态分配都不意味着“非动态”。你所描述的内容应该使用术语“自动分配”(也就是栈上的东西)。 - davec
显示剩余8条评论

816
  • 栈和堆都是从操作系统分配的内存空间(通常是按需映射到物理内存的虚拟内存)。
  • 在多线程环境中,每个线程将有自己完全独立的栈,但它们将共享堆。并发访问必须在堆上进行控制,而在栈上不可能。
    • 堆包含一个已用块和空闲块的链接列表。通过从空闲块之一创建合适的块来满足对堆的新分配(使用newmalloc)。这需要更新堆上块的列表。关于堆上块的这些元信息通常也存储在堆上,通常存储在每个块前面的小区域中。
    • 随着堆变大,经常会从较低地址向较高地址的方向分配新块。因此,您可以将堆视为一个随着内存分配而增长的内存块“堆”。如果堆对于分配来说太小,则可以通过从底层操作系统获取更多内存来扩大其大小。
    • 分配和释放许多小块可能会导致堆处于状态,即许多小的空闲块夹杂在已用块之间。请求分配大块可能会失败,因为没有空闲块足够大以满足分配请求,即使空闲块的组合大小足够大也是如此。这称为“堆碎片”。
    • 当与空闲块相邻的已用块被释放时,新的空闲块可以与相邻的空闲块合并,从而创建一个更大的空闲块,有效地减少了堆的碎片。

    堆

    • 堆栈通常与CPU上的一个名为堆栈指针的特殊寄存器密切协作。最初,堆栈指针指向堆栈的顶部(堆栈上的最高地址)。
    • CPU有特殊的指令用于将值压入堆栈和从堆栈中弹出值。每个压入操作将值存储在堆栈指针的当前位置并减少堆栈指针。弹出操作检索堆栈指针指向的值,然后增加堆栈指针(不要被添加值到堆栈会减少堆栈指针而删除值会增加它所迷惑。请记住,堆栈向底部生长)。存储和检索的值是CPU寄存器的值。
    • 如果函数有参数,则在调用函数之前将这些参数推送到堆栈上。然后,函数中的代码可以从当前堆栈指针向上遍历堆栈以定位这些值。
    • 当调用函数时,CPU使用特殊指令将当前指令指针推送到堆栈上,即正在堆栈上执行的代码的地址。然后,CPU通过设置指令指针到被调用函数的地址来跳转到该函数。稍后,当函数返回时,旧的指令指针从堆栈中弹出,并在调用函数后恢复执行。
    • 当进入函数时,堆栈指针会减小以为本地(自动)变量分配更多空间。如果函数有一个32位的本地变量,则会在堆栈上留下4个字节。当函数返回时,堆栈指针将移回以释放已分配的区域。
    • 嵌套函数调用工作得非常好。每次新调用都会为函数参数、返回地址和局部变量分配激活记录并将这些记录堆叠起来进行嵌套调用,并在函数返回时以正确的方式展开。
    • 由于堆栈是一块有限的内存,如果调用太多嵌套函数和/或为本地变量分配太多空间,就会导致 堆栈溢出。通常,用于堆栈的内存区域设置方式是写入堆栈底部(最低地址)下方,则会在 CPU 中触发陷阱或异常。然后运行时可以捕获这种异常情况并将其转换为某种堆栈溢出异常。

    堆栈

    一个函数能否被分配到堆上而不是堆栈上?

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

    堆是如何管理的取决于运行时环境。C 语言使用 malloc,C++ 使用 new,但许多其他语言都有垃圾回收。

    但是,堆栈是与处理器体系结构密切相关的更低级别的特性。当空间不足时,增加堆并不太困难,因为可以在处理堆的库调用中实现。然而,增加堆栈往往是不可能的,因为只有在过度使用时才会发现堆栈溢出;关闭执行线程是唯一可行的选择。


    42
    @Martin - 您的答案/解释比抽象的被接受的答案更好。展示栈指针/寄存器在函数调用中使用的汇编程序示例将更具说明性。 - Bikal Lem
    5
    每种引用类型都由值类型(int、string等)组成。因为值类型存储在堆栈中,那么当它们成为引用类型的一部分时,它是如何工作的? - Nps
    23
    我认为这个答案是最好的,因为它帮助我理解了什么是返回语句以及它与我每时每刻都遇到的“返回地址”的关系,它意味着将一个函数推入栈中,以及为什么需要将函数推入栈中。很棒的答案! - Alex
    6
    在我看来,这是最好的回答,因为它特别提到堆栈在不同实现中有很大差异。其他答案假设了很多关于编程语言以及环境/操作系统的事情。加一分。 - Qix - MONICA WAS MISTREATED
    3
    你说的“函数中的代码可以从当前堆栈指针向上导航,以定位这些值。”是什么意思?你能详细解释一下吗? - Koray Tugay
    显示剩余12条评论

    448

    以下是C#代码

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

    以下是内存如何管理的方式:

    Picture of variables on the stack

    本地变量只需在函数调用期间保留,将被放入堆栈中。堆用于生存期我们事先不太知道但我们希望它们持续一段时间的变量。在大多数语言中,如果要将变量存储在堆栈上,则需要在编译时知道变量的大小。

    对象(因为我们无法在创建时知道它们存活的时间)会被放入堆中。在许多语言中,堆会进行垃圾回收以查找不再具有任何引用的对象(例如cls1对象)。

    在Java中,大多数对象直接放入堆中。在C/C++等语言中,结构体和类通常在处理指针时仍可以保留在堆栈上。

    更多信息可在以下链接中找到:

    堆栈与堆内存分配的区别 « timmurphy.org

    和这里:

    在堆栈和堆上创建对象

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

    但请注意,其中可能存在一些不准确之处。


    17
    这是不正确的。i和cls不是“静态”变量,它们被称为“本地”或“自动”变量。这是一种非常重要的区别。请参见 [链接] https://dev59.com/hHVD5IYBdhLWcg3wHn2d#13326916 以获取澄清。 - davec
    10
    我并没有说它们是静态变量,我说的是int和cls1是静态项。它们的内存是静态分配的,因此它们放在堆栈中。这与需要动态内存分配的对象形成对比,后者放在堆上。 - Snowcrash
    13
    我引用的“静态项目...进入堆栈”是完全错误的。静态项目进入数据段,自动变量进入堆栈。 - davec
    16
    另外,撰写那篇CodeProject文章的人并不知道他在说什么。例如,他说“原始类型需要静态类型内存”,这完全不正确。你完全可以在堆上动态分配原始数据类型,只需编写类似于“int array[] = new int [num]”的代码即可,原始数据类型就能在.NET中被动态地分配了。这只是其中几个不准确的观点之一。 - davec
    1
    @SnowCrash,我有一个关于你的图片的问题 - 在分配y之后,我如何访问i?我必须弹出y吗?交换它们?如果有很多本地变量将它们分开怎么办? - confused00
    显示剩余3条评论

    233

    其他答案都避免了解释静态分配的含义。因此,我将解释三种主要的分配形式以及它们通常与堆、栈和数据段的关系。我还将在C/C++和Python中提供一些示例,以帮助人们理解。

    "Static"(也称为静态分配)变量不是在栈上分配的。不要这样假设——很多人之所以这样做,只是因为 "static"听起来很像 "stack"。它们实际上既不在堆栈中,也不在堆中。它们是所谓的数据段的一部分。

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

    作用域指的是代码中哪些部分可以访问一个变量。通常,我们认为有 局部作用域(只能被当前函数访问)和 全局作用域(可以在任何地方访问),尽管作用域可能会变得更加复杂。

    生存期指的是程序执行期间变量的分配和释放时间。通常,我们认为有 静态分配(变量将在整个程序的持续时间内存在,这对于在多个函数调用之间存储相同信息很有用)和 自动分配(变量仅在单个函数调用期间存在,这对于存储仅在函数期间使用且可以一旦完成就丢弃的信息很有用)以及 动态分配(变量的持续时间在运行时定义,而不是像静态或自动分配那样在编译时定义)。

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

    我将提供一些简单的注释的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具有静态分配。这不是直观的!因此,我尽量永远不使用单词“static”来描述范围,而是说类似于“文件”或“文件限定”的范围。但是许多人使用短语“静态”或“静态范围”来描述只能从一个代码文件访问的变量。在生命周期的上下文中,“静态”始终意味着该变量在程序启动时分配并在程序退出时解除分配。

    有些人认为这些概念是特定于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
    

    2
    我会将在函数内声明的静态变量称为仅具有本地可访问性,但通常不会使用“作用域”一词。另外,值得注意的是,涉及堆栈/堆的方面,语言基本上没有灵活性:将执行上下文保存在堆栈上的语言无法使用同一个堆栈来保存需要超出它们创建时所在上下文的东西。像 PostScript 这样的一些语言具有多个堆栈,但具有更像堆栈的行为的“堆”。 - supercat
    @supercat,这一切都很有道理。我将作用域定义为“代码中可以访问变量的部分”(并认为这是最标准的定义),所以我认为我们达成了共识 :) - davec
    2
    你一定是在开玩笑吧。你真的可以在函数内定义静态变量吗? - Zaeem Sattar
    2
    @zaeemsattar 当然,这在C代码中并不罕见。 - davec
    1
    @ZaeemSattar 将静态函数变量视为隐藏的全局变量或私有静态成员变量。 - Tom Leys
    显示剩余4条评论

    229

    当你调用一个函数时,该函数的参数以及一些其他开销被放置在栈中。还会存储一些信息(例如返回地址)。 当你在函数内部声明一个变量时,该变量也会被分配在栈上。

    释放栈非常简单,因为总是按照分配的相反顺序进行释放。随着进入函数,栈空间增加,退出函数时对应的数据被移除。这意味着你倾向于在栈的一个小区域内操作,除非你调用了很多调用其他函数的函数(或创建递归解决方案)。

    堆是一个通用名称,用于存放在程序运行期间创建的数据。如果你不知道你的程序将创建多少太空船,你可能会使用new(或malloc或等效)运算符来创建每个太空船。这种分配可能要持续一段时间,因此我们释放它们的顺序可能与创建它们的顺序不同。

    因此,堆远比栈复杂,因为有未使用的内存区域与被碎片化的内存块交错。查找所需大小的空闲内存是一个困难的问题。这就是为什么应该避免使用堆(尽管它仍经常被使用)。

    实现 栈和堆的实现通常由运行时/操作系统处理。通常,对于关键性能的游戏和其他应用程序会创建自己的内存解决方案,从堆中获取大块内存,然后在内部进行分配以避免依赖操作系统的内存。

    只有当你的内存使用情况与常规情况非常不同(例如,在游戏中,你一次加载一个级别并可以在另一个巨大的操作中丢弃整个级别时),这才是实际可行的。

    物理内存位置

    由于一种名为虚拟内存的技术存在,所以此内容并不像您想象中那么相关。该技术使您的程序认为您可以访问某个地址,而实际上物理数据在其他地方(甚至在硬盘上!)。随着调用树的加深,您获得的堆栈地址会按递增顺序排列。堆的地址是不可预测的(即实现特定),并且实际上并不重要。

    18
    避免使用堆的建议相当强烈。现代系统具有良好的堆管理器,现代动态语言广泛使用堆(程序员不必担心)。我建议使用堆,但要使用手动分配器,并不要忘记释放! - Greg Hewgill
    3
    如果可以使用栈或堆,请使用栈。如果无法使用栈,则真的没有选择。我经常同时使用两者,当然使用std::vector或类似的东西会影响堆。对于新手来说,应避免使用堆,因为栈实在是太容易了! - Tom Leys
    如果你的编程语言没有实现垃圾回收机制,那么智能指针(分别分配的对象,包装了一个指针,对动态分配的内存块进行引用计数)与垃圾回收密切相关,并且是一种安全、无泄漏地管理堆的不错方式。它们已经在各种框架中实现,但也可以很容易地为自己的程序实现。 - BenPen
    1
    这就是为什么应该避免使用堆(尽管它仍然经常被使用)。我不确定这实际上意味着什么,特别是在许多高级语言中内存管理方式不同的情况下。由于这个问题标记为与语言无关,我认为这个特定的注释/行是不合适和不适用的。 - LintfordPickle
    3
    不错的观点 @JonnoHampson - 虽然你的观点有道理,但我认为如果你使用带垃圾回收机制的“高级语言”,你可能根本不关心内存分配机制,因此也不介意栈和堆是什么。 - Tom Leys

    178

    其他人已经回答了大致情况,所以我会补充一些细节。

    1. 堆栈并不一定是单数。如果进程中有多个线程,则通常情况下会有多个堆栈。你也可以拥有多个堆,例如某些DLL配置可能导致不同的DLL从不同的堆中分配内存,这就是为什么释放由不同库分配的内存通常是一个坏主意。

    2. 在C语言中,你可以通过使用alloca来获得可变长度分配的好处,它在栈上分配,而不是在堆上分配。这段内存不会在函数返回后保留,但对于临时缓冲区非常有用。

    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
    @PeterMortensen 这不是 POSIX,可移植性不能保证。 - Don Neufeld

    150

    其他人已经直接回答了你的问题,但是当试图理解堆栈和堆时,我认为考虑传统UNIX进程(没有线程和基于mmap()分配器)的内存布局很有帮助。 Memory Management Glossary这个网页上有一张这种内存布局的图示。

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

    在没有虚拟内存的系统中,例如某些嵌入式系统,相同的基本布局通常适用,除了堆栈和堆的大小是固定的。然而,在其他嵌入式系统(如基于Microchip PIC微控制器的系统)中,程序堆栈是一个单独的内存块,不能通过数据移动指令寻址,只能通过程序流程指令(call,return等)间接修改或读取。其他架构,例如Intel Itanium处理器,具有多个堆栈。从这个意义上说,堆栈是CPU架构的一个元素。


    128

    什么是栈?

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

    图片描述

    在计算机架构中,栈是以后进先出方式添加或移除数据的内存区域。
    在多线程应用程序中,每个线程将有其自己的栈。

    什么是堆?

    堆是一个零散无序地堆放着东西的集合。

    图片描述

    在计算机架构中,堆是由操作系统或内存管理库自动管理的动态分配的内存区域。
    堆上的内存在程序执行期间经常被分配、释放和调整大小,这可能导致一种称为“碎片化”的问题。
    碎片化发生在内存对象之间分配了太小的空间,无法容纳其他内存对象的情况下。
    结果是一定比例的堆空间不能用于进一步的内存分配。

    两者如何共存?

    在多线程应用程序中,每个线程将有其自己的栈。但是,所有不同的线程将共享堆。
    因为不同的线程在多线程应用程序中共享堆,这也意味着它们必须协调以避免同时尝试访问和操作堆中相同的内存段。

    哪一个更快——栈还是堆?为什么?

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

    对于新手来说,使用栈可能是一个不错的选择,因为它更容易操作。
    由于栈的空间比较小,所以当你知道需要使用多少内存来存储数据,或者你的数据量很小时,可以考虑使用栈。
    而如果你需要大量内存来存储数据,或者你不确定需要多少内存(例如使用动态数组),则最好使用堆。

    Java 内存模型

    Enter image description here

    栈是存储局部变量(包括方法参数)的内存区域。对于对象变量,它们只是指向堆上实际对象的引用(指针)。
    每次实例化一个对象时,都会在堆内存中预留一段空间来存储该对象的数据(状态)。因为对象可能包含其他对象,因此这些数据实际上可能包含对这些嵌套对象的引用。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接