何时最好使用堆栈而不是堆,反之亦然?

86

在 C++ 中,什么时候最好使用堆栈(stack)?什么时候最好使用堆(heap)?


13
我猜你指的是用于分配内存的系统堆栈和系统堆,而不是数据结构堆和栈,对吗? - Doug T.
7个回答

83

如果变量在当前函数返回后不再使用,请使用栈。如果变量中的数据需要超出当前函数的生命周期,则使用堆。


7
虽然有些绕,但是还是有方法可以解决这个问题。将一个缓冲区传递给一个函数,让函数将数据写入其中,这是一种很好的方式,可以使得函数“返回”动态数据,并在较低的栈帧中继续存在。虽然这种做法不太符合面向对象的风格,但却更加高效。 - Kyle Cronin
9
尺寸也是一个需要考虑的因素:堆栈上的任何大小超过1K都应仔细考虑。有时候最好使用指向堆内存的堆栈指针(连同“资源获取即初始化”惯用语)。 - user3458
3
当内存是一个类的属性时,如何决定类属性应该是指针还是其他类型?此外,什么情况下会使用智能指针? - JagWire
1
来到2020年,我想指出这已经不是一个好的实践方法了:由于移动和复制省略语义,将局部变量传递给调用者几乎总是可以高效地完成。这也消除了手动管理堆中任何内容所带来的删除困扰。 - Rasmus Damgaard Nielsen

42

一般情况下,避免在堆栈上创建过大的对象。

  • 在堆栈上创建对象可以免去记得清理(即删除)对象的负担。但是,在堆栈上创建过多的对象将增加堆栈溢出的可能性。
  • 如果您在堆上使用对象,您会获得操作系统提供的尽可能多的内存,比堆栈要大得多,但是您必须确保在完成后释放内存。此外,在堆中过于频繁地创建对象将倾向于破碎内存,从而影响应用程序的性能。

1
由于这仍然是一个在2020年可以找到的高票问题,请注意许多在栈上分配的容器(如std::vector)实际上会在堆中分配其后备存储。因此,只有当您确实拥有具有数百个属性的类时(您可能本来就不应该拥有),才需要考虑这个问题。 - Rasmus Damgaard Nielsen

20

当你使用的内存严格限制在你创建它的范围内时,使用堆栈是很有用的。这样可以避免内存泄漏,因为你知道在哪里使用内存,也知道何时不再需要它,所以内存会被自动清理。

int main()
{ 
   if (...)
   {
      int i = 0;
   }
   // I know that i is no longer needed here, so declaring i in the above block 
   // limits the scope appropriately
}

然而,堆在内存可能会在其创建范围之外被访问且不希望复制栈变量时非常有用。这可以让您明确控制内存的分配和释放。

Object* CreateObject();

int main()
{
    Object* obj = CreateObject();
    // I can continue to manipulate object and I decide when I'm done with it

    // ..
    // I'm done
    delete obj;
    // .. keep going if you wish
    return 0;
}

Object* CreateObject()
{
   Object* returnValue = new Object();
   // ... do a bunch of stuff to returnValue
   return returnValue;
   // Note the object created via new here doesn't go away, its passed back using 
   // a pointer
}

显然,一个常见的问题是您可能忘记删除对象。这被称为内存泄漏。随着程序变得越来越不平凡,这些问题变得更加普遍,其中“所有权”(或者谁负责删除事物)变得更加难以定义。
在更受管理的语言(C#,Java)中,常见的解决方案是实现垃圾回收,因此您无需考虑删除内容。但是,这意味着后台有一些运行时间不定的东西来检查您的堆数据。在非平凡程序中,当“垃圾收集”线程弹出并运行时,这可能会变得相当低效,它会寻找应该删除的数据,而您的程序的其余部分将被阻止执行。
在C ++中,处理内存泄漏最常见且最好(我认为)的解决方案是使用智能指针。其中最常见的是boost :: shared_ptr,它是(引用计数
因此,要重新创建上面的示例 boost :: shared_ptr CreateObject();
int main()
{
    boost::shared_ptr<Object> obj = CreateObject();
    // I can continue to manipulate object and I decide when I'm done with it

    // ..
    // I'm done, manually delete
    obj.reset(NULL);
    // .. keep going if you wish
    // here, if you forget to delete obj, the shared_ptr's destructor will note
    // that if no other shared_ptr's point to this memory 
    // it will automatically get deleted.
    return 0;
}

boost::shared_ptr<Object> CreateObject()
{
   boost::shared_ptr<Object> returnValue(new Object());
   // ... do a bunch of stuff to returnValue
   return returnValue;
   // Note the object created via new here doesn't go away, its passed back to 
   // the receiving shared_ptr, shared_ptr knows that another reference exists
   // to this memory, so it shouldn't delete the memory
}

9

一般来说,尽可能使用堆栈。例如,在该变量在该范围之外不需要时。

这样更快,引起的碎片也较少,并且可以避免与调用malloc或new相关的其他开销。从堆栈分配只需要几个汇编操作,而malloc或new则需要在高效实现中数百行代码。

虽然无法避免,但从未最好使用堆... :)


这比几个汇编操作更好 - 它只是一个加法或减法(取决于您的堆栈增长方向)。 - Eclipse
加减操作并不总是单个操作...还要考虑到另一端的清理工作。根据调用约定,将会有一个sub/add与add/sub相匹配,尽管这些可能会合并,具体取决于您如何使用堆栈以及编译器进行了哪些优化(它实际上可能归结为零条指令...或在非常特殊的情况下,负指令)。 - jheriko

9

以上提到的规则有一个例外,即通常应该使用堆栈存储本地变量,这些变量在函数范围之外不需要使用:

如果递归函数分配了大量本地变量或被多次递归调用,可能会耗尽堆栈空间。如果您有一个利用内存的递归函数,使用基于堆的内存而不是基于堆栈的内存可能是一个好主意。


3

在运行时仅使用堆来分配对象空间。如果您在编译时知道大小,请使用堆栈。不要从函数返回堆分配的对象,而是将缓冲区传递到函数中以进行写入。这样,缓冲区可以在调用函数的位置作为数组或其他基于堆栈的结构进行分配。

您拥有的malloc()语句越少,内存泄漏的机会就越少。


0

有些情况下你需要使用栈,有些情况下你需要使用堆,有些情况下你需要使用静态存储,有些情况下你需要使用常量内存数据,有些情况下你需要使用自由存储。

栈很快,因为分配只是在SP上的“增量”,并且所有的“分配”都是在函数调用时执行的。堆(或自由存储)的分配/释放更加耗时且容易出错。


抱歉,但第一段真的很模糊不清。 - Maf

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