题 堆栈和堆的内容和位置是什么?


编程语言书籍解释了在类型上创建的值类型 ,并在上创建引用类型 ,没有解释这两件事是什么。我还没有看到对此的明确解释。我明白了什么 一堆 是。但,

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

7126
2017-09-17 04:18


起源


这里可以找到一个非常好的解释 堆栈和堆之间有什么区别? - Songo
还(真的)好: codeproject.com/Articles/76153/... (堆栈/堆部分) - Ben
可以找到很好的解释 这里 - Bharat
youtube.com/watch?v=clOUdVDDzIM&spfreload=5 - Selvamani
相关,请参阅 堆栈冲突。 Stack Clash补救措施影响了系统变量和行为的某些方面 rlimit_stack。另见Red Hat 问题1463241 - jww


答案:


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

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

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

直接回答您的问题:

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

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

它们的范围是什么?

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

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

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

是什么让一个更快?

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

明确的示范:
图片来源: vikashazrati.wordpress.com


5239
2017-09-17 04:52



很好的答案 - 但我认为你应该补充一点,当进程启动时由操作系统分配堆栈(假设存在操作系统),它由程序保持内联。这是堆栈速度更快的另一个原因 - 推送和弹出操作通常是一个机器指令,而现代机器可以在一个周期中至少完成3个,而分配或释放堆涉及调用OS代码。 - sqykly
我最后对图表感到困惑。我以为直到看到那张照片才知道。 - Sina Madani
@Anarelle处理器在有或没有操作系统的情况下运行指令。一个接近我心的例子是SNES,它没有API调用,没有我们今天所知的操作系统 - 但它有一个堆栈。在堆栈上进行分配是在这些系统上进行加法和减法,这对于通过从创建它们的函数返回而弹出的变量来说是很好的,但是将其与构造函数进行对比,其结果不仅仅是扔掉了。为此,我们需要堆,它与call和return无关。大多数操作系统都有API堆,没有理由自己做 - sqykly
“堆栈是作为临时空间留出的内存”。凉。但它在Java内存结构方面实际上“搁置”在哪里?是堆内存/非堆内存/其他(Java内存结构如下) betsol.com/2017/06/... ) - Jatin Shashoo


堆栈:

  • 像堆一样存储在计算机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;

2095
2017-09-17 04:20



指针pBuffer和b的值位于堆栈上,并且很可能在函数的入口处分配。根据编译器的不同,缓冲区也可以在功能入口处分配。 - Andy
这是一种常见的误解 C 语言,由...定义 C99 语言标准(可在 open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf ),需要“堆叠”。实际上,“堆栈”这个词甚至没有出现在标准中。这回答了wrt / to的陈述 C一般来说,堆栈的使用是正确的,但语言并不要求它。看到 knosof.co.uk/cbook/cbook.html 了解更多信息,特别是如何 C 在奇球结构上实现,例如 en.wikipedia.org/wiki/Burroughs_large_systems - johne
@Brian你应该解释一下 为什么 在堆栈上创建了buffer []和pBuffer指针,以及为什么在堆上创建pBuffer的数据。我认为有些人可能会对你的答案感到困惑,因为他们可能认为程序是专门指示在堆栈上分配内存而不是堆,但事实并非如此。是因为Buffer是值类型而pBuffer是引用类型? - Howiecamp
@Remover:没有指针包含一个地址,它可以指向堆或堆栈上的某些东西。 new,malloc和类似于malloc的其他一些函数在堆上分配并返回已分配的内存的地址。你为什么要在堆上分配?这样你的记忆就不会超出范围,直到你想要它才会被释放。 - Brian R. Bondy
“负责内存泄漏” - 堆不负责内存泄漏! Lazy / Forgetful / ex-java编码员/编码员谁不给垃圾! - Laz


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

  • 在一堆物品中,物品按照它们放置在那里的顺序一个在另一个上面,你只能移除顶部的物品(不会翻倒整个物品)。

    Stack like a stack of papers

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

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

    Heap like a heap of licorice allsorts

    堆分配需要维护已分配内存和不分配内存的完整记录,以及减少碎片的一些开销维护,找到足以满足请求大小的连续内存段,等等。内存可以在任何时候释放,留下空闲空间。有时,内存分配器将执行维护任务,例如通过移动已分配的内存来对内存进行碎片整理,或者进行垃圾收集 - 在内存不再占用范围并在解除分配时在运行时进行识别。

这些图像应该可以很好地描述在堆栈和堆中分配和释放内存的两种方法。百胜!

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

    如上所述,堆和堆栈是通用术语,可以通过多种方式实现。计算机程序通常有一个名为a的堆栈 调用堆栈 它存储与当前函数相关的信息,例如指向调用它的函数的指针,以及任何局部变量。因为函数调用其他函数然后返回,所以堆栈增大和缩小以保持来自调用堆栈中的函数的信息。程序实际上没有对它进行运行时控制;它取决于编程语言,操作系统甚至系统架构。

    堆是一个通用术语,用于动态和随机分配的任何内存;即无序。内存通常由OS分配,应用程序调用API函数来执行此分配。管理动态分配的内存需要相当大的开销,这通常由操作系统处理。

  • 它们的范围是什么?

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

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

    同样,它取决于语言,编译器,操作系统和体系结构。堆栈通常是预先分配的,因为根据定义它必须是连续的内存(更多内容在最后一段中)。语言编译器或OS确定其大小。您不会在堆栈上存储大量数据,因此除非出现不必要的无限递归(因此,“堆栈溢出”)或其他异常编程决策,否则它应该永远不会被完全使用。

    堆是可以动态分配的任何东西的通用术语。根据您看待它的方式,它会不断变化大小。在现代处理器和操作系统中,它的工作方式无论如何都是非常抽象的,所以你通常不需要担心它如何深入工作,除了(在它允许你的语言中)你不能使用的内存你尚未分配或已释放的记忆。

  • 是什么让一个更快?

    堆栈更快,因为所有可用内存始终是连续的。不需要维护所有可用内存段的列表,只需要指向当前堆栈顶部的单个指针。编译器通常将此指针存储在特殊的快速指针中 寄存器 以此目的。更重要的是,堆栈上的后续操作通常集中在非常靠近的内存区域,这非常低的水平有利于处理器片上高速缓存的优化。


1261
2018-03-19 14:38



大卫我不同意这是一个好的形象,或者“下推堆栈”是一个很好的术语来说明这个概念。当您向堆栈添加内容时,堆栈的其他内容 不 推下来,他们仍然保持原状。 - thomasrutter
这个答案包含一个大错误。静态变量未在堆栈上分配。看我的回答[链接] stackoverflow.com/a/13326916/1763801 为了澄清。您将“自动”变量与“静态”变量等同起来,但它们完全不相同 - davec
具体来说,您说在堆栈上分配了“静态分配的局部变量”。实际上它们是在数据段中分配的。只有自动分配的变量(包括大多数但不是所有局部变量以及像值而不是通过引用传递的函数参数之类的东西)都会在堆栈上分配。 - davec
我刚刚意识到你是对的 - 在C中, 静态分配 是它自己独立的东西,而不是任何不是的东西 动态。我编辑了我的答案,谢谢。 - thomasrutter
它不仅仅是C. Java,Pascal,Python和其他许多人都有静态与自动与动态分配的概念。说“静态分配”在任何地方都意味着同样的事情。在任何语言中静态分配都意味着“不动态”。您希望术语“自动”分配您所描述的内容(即堆栈中的内容)。 - davec


(我已经从另一个或多或少是这个问题的问题中提出了这个答案。)

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

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

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

The heap

堆栈

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

The stack

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

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

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

但是,堆栈是与处理器架构紧密相关的更低级别的功能。当没有足够的空间时增加堆不是太难,因为它可以在处理堆的库调用中实现。但是,堆栈的增长通常是不可能的,因为只有在为时已晚时才发现堆栈溢出;并且关闭执行线程是唯一可行的选择。


664
2017-07-31 15:54



@Martin - 一个非常好的答案/解释,而不是更抽象的答案。显示用于vis函数调用的堆栈指针/寄存器的示例汇编程序将更具说明性。 - Bikal Lem
每个引用类型都是值类型的组合(int,string等)。正如所说的那样,值类型存储在堆栈中,而不是它们作为引用类型的一部分时的工作方式。 - Nps
这个答案在我看来是最好的,因为它帮助我理解了返回语句究竟是什么以及它与我偶尔遇到的这个“返回地址”有什么关系,将函数推入堆栈意味着什么,以及为什么函数被压入堆栈。很棒的答案! - Alex
这是我认为最好的,即提到堆/堆栈 非常 具体实施。其他答案假设a 批量 关于语言和环境/操作系统的事情。 +1 - Qix
你是什​​么意思“函数中的代码然后能够从当前堆栈指针向上导航堆栈以找到这些值。” ?你能详细说明一下吗? - Koray Tugay


在以下C#代码中

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

以下是内存的管理方式

Picture of variables on the stack

Local Variables 只要函数调用进入堆栈,只需要持续。堆用于变量,我们事先并不知道它们的生命周期,但我们希望它们可以持续一段时间。在大多数语言中,如果我们想要将它存储在堆栈中,那么在编译时我们知道变量的大小是至关重要的。

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

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

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

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

和这里:

在堆栈和堆上创建对象

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

但请注意,它可能包含一些不准确之处。


352
2017-11-09 12:28



这是不正确的。我和cls不是“静态”变量。它们被称为“本地”或“自动”变量。这是一个非常重要的区别。见[link] stackoverflow.com/a/13326916/1763801 为了澄清 - davec
我没有说他们是静止的 变量。我说int和cls1是静态的 项目。他们的内存是静态分配的,因此它们会进入堆栈。这与需要动态内存分配的对象形成对比,因此动态内存分配在堆上。 - Snowcrash
我引用“静态项目......进入堆栈”。这只是错误的。静态项目进入数据段,自动项目进入堆栈。 - davec
编写该代码项目文章的人也不知道他在说什么。例如,他说“原始人需要静态类型记忆”,这完全是不真实的。没有什么能阻止你动态地在堆中分配原语,只需写一些类似“int array [] = new int [num]”的东西,瞧,在.NET中动态分配的原语。这只是几个不准确之一。 - davec
我编辑了你的帖子,因为你对堆栈和堆中的内容犯了严重的技术错误。 - Tom Leys


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

取消分配堆栈非常简单,因为您总是按照分配的相反顺序解除分配。输入函数时会添加堆栈内容,退出时会删除相应的数据。这意味着您倾向于保持在堆栈的一个小区域内,除非您调用许多调用许多其他函数的函数(或创建递归解决方案)。

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

因此,堆要复杂得多,因为最终存在未使用的内存区域与内存被分段的块交织。找到所需大小的空闲内存是一个难题。这就是应该避免堆的原因(虽然它仍然经常使用)。

履行 堆栈和堆的实现通常都是运行时/操作系统。通常,性能至关重要的游戏和其他应用程序会创建自己的内存解决方案,从堆中获取大量内存,然后在内部将其清除,以避免依赖操作系统获取内存。

这只有在你的内存使用量与标准有很大不同的情况下才有用 - 例如,你在一个巨大的操作中加载一个级别的游戏,并且可以在另一个巨大的操作中丢掉所有内存。

内存中的物理位置 由于所谓的技术,这与您的想法相关性较低 虚拟内存 这使得您的程序认为您可以访问某个地址,其中物理数据位于其他位置(即使在硬盘上!)。随着调用树的深入,您获得的堆栈地址会逐渐增加。堆的地址是不可预测的(即特定的实施),坦率地说并不重要。


191
2017-09-17 04:27



建议避免使用堆非常强大。现代系统具有良好的堆管理器,现代动态语言广泛使用堆(没有程序员真正担心它)。我会说使用堆,但使用手动分配器,不要忘记免费! - Greg Hewgill
如果可以使用堆栈或堆,请使用堆栈。如果你不能使用堆栈,真的别无选择。我使用了很多,当然使用std :: vector或类似命中堆。对于新手来说,你可以避免堆积,因为堆栈非常简单! - Tom Leys
如果你的语言没有实现垃圾收集,那么智能指针(Seporately分配的对象包围指针,它们为动态分配的内存块引用计数)与垃圾收集密切相关,是一种在保险箱中管理堆的好方法并且无泄漏的方式。它们在各种框架中实现,但也不是很难为您自己的程序实现。 - BenPen
“这就是应该避免堆的原因(虽然它仍然经常被使用)。”我不确定这实际意味着什么,尤其是在许多高级语言中管理内存的方式不同。由于这个问题被标记为与语言无关,我会说这个特定的评论/行不合适且不适用。 - JonnoHampson
好点@JonnoHampson - 当你提出一个有效的观点时,我认为如果你使用GC的“高级语言”工作,你可能根本不关心内存分配机制 - 所以不要甚至关心堆栈和堆是什么。 - Tom Leys


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

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

但是,通常最好考虑“范围“和”一生“而不是”堆叠“和”堆积“。

范围是指代码的哪些部分可以访问变量。一般我们会想到 当地范围 (只能通过当前函数访问)与 全球范围 (可以在任何地方访问)虽然范围可以变得更加复杂。

生命周期是指在程序执行期间分配和取消分配变量的时间。通常我们会想到 静态分配 (变量将持续整个程序的整个持续时间,使其对于在多个函数调用中存储相同的信息非常有用) 自动分配 (变量仅在单次调用函数期间持续存在,使其对存储仅在函数期间使用的信息很有用,并且一旦完成就可以丢弃) 动态分配 (持续时间在运行时定义的变量,而不是静态或自动的编译时间)。

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

我将提供一些简单的带注释的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

169
2017-09-17 04:48



我会将函数中声明的静态变量称为仅具有局部变量 无障碍,但通常不会使用“范围”一词。此外,值得注意的是,语言基本上没有灵活性的一个堆栈/堆方面:在堆栈上保存执行上下文的语言不能使用相同的堆栈来保存需要比创建它们的上下文更长的内容。有些语言喜欢 PostScript 有多个堆栈,但有一个“堆”,其行为更像堆栈。 - supercat
@supercat这一切都有道理。我将范围定义为“代码的哪些部分可以 访问 一个变量“(感觉这是最标准的定义)所以我认为我们同意:) - davec
我会考虑一个“范围” 变量 由于时间和空间的限制。只要对象存在,类对象范围内的变量就需要保持其值。只要执行保留在该上下文中,执行上下文范围内的变量就需要保持其值。静态变量声明创建一个 识别码 其范围与当前块有关,该块附加到a 变量 其范围无限。 - supercat
@supercat这就是为什么我使用生命周期这个词,这就是我称之为时间范围的方式。它减少了重载“范围”一词的需要,因为它有很多含义。据我所知,即使在规范来源中,似乎也没有就确切的定义达成共识。我的术语部分来自K&R,部分来自我研究/教授的第一个CS部门的主要用法。总是很高兴听到另一个明智的观点。 - davec
你一定在开玩笑。你能真正在函数内定义静态变量吗? - Zaeem Sattar


其他人已经很好地回答了广泛的笔触,所以我会提出一些细节。

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

  2. 在C中,您可以通过使用来获得可变长度分配的好处 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.
}

156
2017-09-17 07:16



重新“而不是分配”:你的意思是“与malloc相对”? - Peter Mortensen
便携性如何 alloca? - Peter Mortensen
@PeterMortensen它不是POSIX,不保证可移植性。 - Don Neufeld


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

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

在没有虚拟内存的系统中,例如某些嵌入式系统,通常会应用相同的基本布局,但堆栈和堆的大小是固定的。但是,在其他嵌入式系统(例如基于Microchip PIC单片机的系统)中,程序堆栈是一个单独的内存块,无法通过数据移动指令寻址,只能通过程序流指令间接修改或读取(调用,返回等)。其他架构,如Intel Itanium处理器,也有 多个堆栈。从这个意义上说,堆栈是CPU架构的一个元素。


126
2017-09-17 04:57





我想很多其他人在这件事上给了你大部分正确答案。

然而,遗漏的一个细节是“堆”实际上可能被称为“免费商店”。这种区别的原因是原始的免费存储是使用称为“二项式堆”的数据结构实现的。因此,从malloc()/ free()的早期实现中分配是从堆中分配的。然而,在这个现代,大多数免费商店都使用非二维堆的非常精细的数据结构来实现。


108
2017-09-17 04:29



另一个挑剔 - 大多数答案(轻微)意味着需要使用“堆栈” C 语言。这是一种常见的误解,尽管它是(迄今为止)主导的实施范式 C99 6.2.4 automatic storage duration objects (变量)。事实上,“堆叠”这个词甚至没有出现在 C99 语言标准: open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf - johne
[@Heath]我对你的答案有一点评论。看看接受的答案 这个问题。它说的 免费商店  最可能 和...一样 堆,虽然不一定是。 - OmarOthman


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

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

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

因为你在启动程序之前已经分配了堆栈,所以在使用堆栈之前你永远不需要malloc,所以这是一个小优势。在实践中,很难预测具有虚拟内存子系统的现代操作系统的速度和速度会有多快,因为页面的实现方式和存储位置是实现细节。


107
2018-06-11 19:42



此外值得一提的是,intel大大优化了堆栈访问,尤其是预测从函数返回的位置。 - Tom Leys