为什么你会想要在堆上分配内存而不是栈上?

24
可能重复:
何时最好使用堆栈而不是堆,反之亦然? 我已经阅读了一些关于堆栈与堆的其他问题,但它们似乎更关注堆/栈做什么,而不是为什么要使用它们。
对我来说,由于堆栈指针移动快(只需移动堆栈指针,而不是在堆中寻找空闲空间),而且在使用完分配的内存后无需手动释放它,因此几乎总是优先选择堆栈分配。唯一我能想到需要使用堆分配的原因是,如果您想在函数中创建一个对象,然后在该函数范围之外使用它,因为从函数返回后,堆栈分配的内存会自动被释放。
除了我不知道的在堆分配而不是堆栈分配时使用的其他原因之外,还有吗?

1
重复:https://dev59.com/LXVD5IYBdhLWcg3wE3No - Naveen
6
@Naveen:有点类似 - 另一个是有关 C++ 的问题,而这里讨论的是 C 语言,因此一些答案涉及到“delete”和“new”,这在 C 语言中不适用。 - Jonathan Leffler
9个回答

39

有几个原因:

  • 主要原因是,使用堆分配可对对象的生命周期(从 malloc/callocfree)进行最灵活的控制;
  • 栈空间在默认配置下通常比堆空间更受限制;
  • 无法分配堆空间可以得到妥善处理,而堆栈空间不足往往是无法恢复的。

如果没有灵活的对象生命周期,诸如二叉树和链表这样有用的数据结构将几乎无法编写。


栈空间通常比堆空间更有限,为什么? - onepiece
@onepiece:这主要取决于默认的进程布局选择和堆栈限制,但还有一个基本事实,即进程中的每个线程都需要为其堆栈划分出一个单一连续的虚拟内存块。 - caf

20
  1. 你希望在函数调用结束后,仍然保留分配的内存空间。
  2. 你想要节省堆栈空间(通常只有几 MB),
  3. 你正在处理可重新定位内存(例如 Win16、数据库等),或者想从分配失败中恢复。
  4. 变量长度不固定。虽然可以使用一些技巧来解决,但代码会变得非常混乱。

其中最重要的是第1点。一旦涉及到任何形式的并发或 IPC,第1点就无处不在。即使是非平凡的单线程应用程序也很难在没有堆分配的情况下设计。这几乎相当于在 C/C++ 中伪造一个函数式语言。


10

我想创建一个字符串。我可以在堆上或栈上创建它。我们来尝试一下:

char *heap = malloc(14);
if(heap == NULL)
  {
    // bad things happened!
  }
strcat(heap, "Hello, world!");

并且对于堆栈:

char stack[] = "Hello, world!";

现在我有这两个字符串分别在它们的位置上。稍后,我想让它们变长:

char *tmp = realloc(heap, 20);
if(tmp == NULL)
  {
    // bad things happened!
  }
heap = tmp;
memmove(heap + 13, heap + 7);
memcpy(heap + 7, "cruel ", 6);

并且对于堆栈:

// umm... What?

这只是其中一个好处,其他人已经提到了其他好处,但这是一个相当不错的好处。使用堆内存,我们至少可以尝试将我们分配的空间变大。对于栈而言,我们只能使用固定大小的空间。如果我们想要更多的空间来增长,我们必须一开始就声明所有需要的空间,而我们都知道这是多么麻烦:

char username[MAX_BUF_SIZE];

4

在使用堆的时候,最明显的理由是当你调用一个函数并需要返回长度未知的内容时。有时候调用者可能会传递内存块和大小给函数,但在其他情况下这是不可行的,特别是如果返回的内容很复杂(例如,一组带有指针的不同对象集合等)。


3

在很多情况下,大小限制是一个巨大的瓶颈。堆栈通常以低兆字节甚至千字节为单位测量(这是针对堆栈上的所有内容),而所有现代PC都允许您使用几个GB的堆。因此,如果您将使用大量数据,则绝对需要堆。


0

堆栈变量(通常称为“自动变量”)最适合用于您希望始终保持相同且始终很小的内容。

int x;
char foo[32];

所有的堆栈分配都是在编译时固定的。

堆分配最好的理由是你不能总是知道需要多少空间。通常只有在程序运行时才能确定。你可能有一个限制的想法,但你只想使用所需的确切空间。

如果你必须读取一个文件,它可能是从1k到50mb的任何大小,你不会这样做:

int readdata ( FILE * f ) {
  char inputdata[50*1024*1025];
  ...
  return x;
}

这将尝试在堆栈上分配50MB的空间,但通常会失败,因为堆栈通常限制在256k以内。


0

只是补充一下,你可以使用alloca在堆栈上分配内存,但是堆栈上的内存有限,而且空间仅存在于函数执行期间。这并不意味着所有东西都应该分配在堆上。像所有设计决策一样,这也有点困难,应该谨慎地综合使用两者。


我必须再次提醒一下“函数x()是非标准函数”的注释。alloca()在许多平台上以某种形式存在,但不属于任何标准。 - Chris Lutz
Chris:这是绝对正确的,然而在堆与栈分配的更广泛讨论中,考虑到某些地方存在alloca()是有用的。特别是因为它清晰地表明了运行时计算分配大小的限制是C语言所强加的限制,而不是基于栈的分配思想本身固有的限制。 - caf

0
除了手动控制对象的生命周期(您提到的),使用堆的其他原因包括:
- 运行时控制对象的大小(在程序执行期间,包括初始大小和“后来”的大小)。
例如,您可以分配一个在运行时才知道大小的数组。
随着C99中可变长度数组(VLA)的引入,即使不使用堆也可以分配固定运行时大小的数组(这基本上是'alloca'功能的语言级实现)。但是,在其他情况下,即使在C99中,您仍然需要堆。
- 运行时控制对象的总数。
例如,当您构建二叉树结构时,无法预先有意义地在堆栈上分配树的节点。您必须使用堆来“按需”分配它们。
- 低级技术考虑,如有限的堆栈空间(其他人已经提到了)。
当您需要一个大的I/O缓冲区,即使只是短时间内(在单个函数内),从堆中请求它而不是声明一个大的自动数组更有意义。

0
堆和栈共享同一块“开放”内存空间,并且如果你使用整个内存段,它们最终会相遇。保持它们使用的空间之间的平衡将在以后分摊分配和释放内存的成本,并使渐进值更小。

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