【问题标题】:Why can't you free variables on the stack?为什么不能释放堆栈上的变量?
【发布时间】:2013-11-13 22:12:03
【问题描述】:

所讨论的语言是 C/C++。

我的教授说在您使用完堆后释放内存,否则您可能会得到无法访问的内存。这样做的问题是您最终可能会用完所有内存,并且您无法访问其中的任何内容。

为什么相同的概念不适用于堆栈?我知道你总是可以访问你在堆栈上使用的内存,但是如果你不断创建新变量,你最终会用完空间对吗?那么为什么不能像在堆上一样释放堆栈上的变量来为新变量腾出空间呢?

我知道编译器释放堆栈上的变量,但那是在变量范围的末尾。它不会在其作用域的末尾释放堆上的变量吗?如果没有,为什么不呢?

【问题讨论】:

  • C/C++ 不是一种语言。
  • 编译器会为你做这些。
  • @dnk 是的,我明白了,但那是在变量范围的末尾。它不是在其作用域的末尾释放堆上的变量吗?如果没有,为什么不呢?
  • @dfg:堆上没有变量这样的东西。您可以有多个指向堆上的内存块的指针。
  • 你需要阅读这个:stackoverflow.com/questions/79923/…

标签: c++ c memory heap-memory stack-memory


【解决方案1】:

动态分配的对象(口语中的“堆对象”)绝不是变量。因此,它们永远不会超出范围。他们不在任何范围内。处理它们的唯一方法是通过分配时获得的指针

(指针通常被分配给一个变量,但这并没有帮助。)

重复:变量有范围; 对象没有。但是很多对象都是变量。

回答这个问题:你只能释放对象,不能释放变量

【讨论】:

  • 只是一个术语问题:作为局部变量的类实例化 (v.g. MyClass localVariable;) 不被视为对象?
  • @SJuan76:是的。对象可以是变量,但不是必须的。变量可以是对象,但不一定是。
  • 真正迂腐的是names。变量是一个命名对象,但它是具有作用域的名称而不是对象。但是,对于自动变量,对象的生命周期与名称的范围相同,这就是为什么谈论变量的“范围”是有意义的,即使它是名称的范围。
  • @SteveJessop:确实,好点 - 变量是“有名字的事物”的适当子集,因此缺乏“全部真相”应该仍然有意义。 “有名字的东西”(我不会在这里称它们为“命名对象”,只是为了避免混淆)当然是一个重要的概念,也是一个很好的下一个研究主题(例如链接)。也许“对象有存储,名称有范围和链接”或类似的东西。
【解决方案2】:

闭合的“}”大括号的末尾是堆栈“释放”其内存的地方。所以如果我有:

{
    int a = 1;
    int b = 2;

    {
        int c = 3; // c gets "freed" at this "}" - the stack shrinks
                   // and c is no longer on the stack.
    }
}                  // a and b are "freed" from the stack at this last "}".

您可以认为 c 在堆栈中比“a”和“b”“更高”,因此 c 在它们之前被弹出。因此,每次你写一个“}”符号时,你实际上是在收缩堆栈并“释放”数据。

【讨论】:

  • 如果括号中的代码占用了太多空间,以至于您在作用域结束之前耗尽了堆栈上的内存,会发生什么?在那种情况下,您不希望能够手动将变量从堆栈中取出吗?
  • @dfg 如果你需要变量来完成计算,你会怎么做?请记住,如果您确实只需要计算的特定部分的变量,则可以将 { } 块放置在要创建本地范围的任何位置。
【解决方案3】:

已经有很好的答案,但我认为您可能需要更多说明,因此我会尝试将其作为更详细的答案并尝试使其简单(如果我设法)。如果有什么不清楚的(我不是以英语为母语的人,有时可能会在制定答案时遇到问题),请在 cmets 中询问。还将采用 Kerrek SB 在他的回答中使用的 VariablesObjects 的想法。

为了更清楚地说明,我认为 变量命名的对象,其中 对象 是在您的程序中存储数据的东西。

堆栈上的变量得到automatic storage duration,一旦作用域结束,它们就会自动被销毁和回收。

{
    std::string first_words = "Hello World!";

    // do some stuff here...

} // first_words goes out of scope and the memory gets reclaimed.

在这种情况下,first_words 是一个变量(因为它有自己的名字),这意味着它也是一个对象

现在堆呢?让我们将您可能认为的“堆上的东西”描述为 变量,它指向 Object 所在的堆上的某个内存位置。现在这些东西得到了所谓的dynamic storage duration

{
    std::string * dirty = nullptr

    {

        std::string * ohh = new std::string{"I don't like this"}    // ohh is a std::string* and a Variable
                                                                    // The actual std::string is only an unnamed
                                                                    // Object on the heap.

        // do something here

        dirty = ohh; // dirty points to the same memory location as ohh does now.

    }   // ohh goes out of scope and gets destroyed since it is a Variable.
        // The actual std::string Object on the heap doesn't get destroyed

    std::cout << *dirty << std::endl;   // Will work since the std::string on the heap that dirty points to
                                        // is still there.

    delete dirty; // now the object being pointed to gets destroyed and the memory reclaimed

    dirty = nullptr; can still access dirty since it's still in its scope.

} // dirty goes out of scope and get destroyed.

如您所见,对象不遵守范围,您必须手动管理它们的内存。这也是为什么“大多数”人更喜欢在它周围使用“包装器”的原因。参见例如 std::string ,它是动态“字符串”的包装器。

现在澄清你的一些问题:

  1. 为什么我们不能销毁堆栈上的对象?

    简单的回答:你为什么想要?

    详细答案:它会被你破坏,一旦离开不允许的范围,它就会再次被破坏。此外,您通常应该只在您的范围内拥有计算实际需要的变量,如果您确实需要该变量来完成计算,您将如何销毁它?但是,如果您真的只需要一个变量在计算中的一小段时间内,您可以使用 { } 创建一个新的更小的范围,这样您的变量就会在不再需要时自动销毁。

    注意:如果您有很多变量,而您只需要进行一小部分计算,则可能暗示该部分计算应该在其自己的函数/范围内。

  2. 来自您的 cmets:是的,我明白了,但那是在变量范围的末尾。它不是也在其作用域结束时释放堆上的变量吗?

    他们没有。堆上的对象没有范围,您可以将它们的地址从函数中传递出来,它仍然存在。指向它的指针可能会超出范围并被销毁,但堆上的对象仍然存在,您无法再访问它(内存泄漏)。这也是它被称为手动内存管理的原因,大多数人更喜欢围绕它们的包装器,以便在不再需要时自动销毁它。以 std::string、std::vector 为例。

  3. 来自您的 cmets:另外,您如何在计算机上耗尽内存?一个 int 占用 4 个字节,大多数计算机都有数十亿字节的内存......(不包括嵌入式系统)?

    嗯,计算机程序并不总是只有几个ints。让我用一点“假”的报价来回答:

    640K [计算机内存] 对于任何人来说都应该足够了。

    但这还不够,我们都应该知道。多少内存就够了?我不知道,但肯定不是我们现在得到的。有许多算法、问题和其他需要大量内存的东西。想想像电脑游戏这样的东西。如果我们有更多的内存,我们可以制作“更大”的游戏吗?想想看……你总是可以用更多的资源做更大的事情,所以我认为我们可以说它没有任何限制。

【讨论】:

    【解决方案4】:

    那么为什么不能像在堆上那样释放堆栈上的变量来为新变量腾出空间呢?

    “堆栈分配器”知道的所有信息都是ESP,它是指向堆栈底部的指针。

       N: used
     N-1: used
     N-2: used
     N-3: used <- **ESP**
     N-4: free
     N-5: free
     N-6: free
     ...
    

    这使得“堆栈分配”非常有效 - 只需将 ESP 减少分配的大小,而且它是局部性/缓存友好的。

    如果您允许不同大小的任意释放 - 这会将您的“堆栈”变成“堆”,并带来所有相关的额外开销 - ESP 是不够的,因为您必须记住哪些空间被释放并且不是:

       N: used
     N-1: free
     N-2: free
     N-3: used
     N-4: free
     N-5: used
     N-6: free
     ...
    

    显然 - ESP 还不够。而且你还必须处理碎片问题。

    我知道编译器释放堆栈上的变量,但那是在变量范围的末尾。它不会在其作用域的末尾释放堆上的变量吗?如果没有,为什么不呢?

    其中一个原因是您并不总是希望这样 - 有时您希望将分配的数据返回给函数的调用者,这些数据应该超出创建它的范围。

    也就是说,如果您真的需要对“堆”分配的数据进行基于范围的生命周期管理(实际上,大多数情况下它是基于范围的) - C++ 中的常见做法是使用围绕此类数据的包装器。一个例子是std::vector

    {
        std::vector<int> x(1024); // internally allocates array of 1024 ints on heap
        // use x
        // ...
    } // at the end of the scope destructor of x is called automatically,
      // which does deallocation
    

    【讨论】:

      【解决方案5】:

      了解函数调用 - 每次调用都会将数据和函数地址压入堆栈。函数从堆栈中弹出数据并最终推送其结果。

      通常,堆栈由操作系统管理,是的 - 它可以被耗尽。试着做这样的事情:

      int main(int argc, char **argv)
           {
           int table[1000000000];
           return 0;
           }
      

      这应该很快结束。

      【讨论】:

      • 你是对的......优化编译器会简单地忽略声明并立即结束。
      【解决方案6】:

      堆栈上的局部变量实际上并没有被释放。指向当前堆栈的寄存器只是向上移动,堆栈“忘记”它们。是的,您可能会占用太多堆栈空间,以至于它会溢出并导致程序崩溃。
      当程序退出时,堆上的变量会被操作系统自动释放。如果你这样做了

      int x;
      for(x=0; x<=99999999; x++) {
        int* a = malloc(sizeof(int));
      }
      

      a 的值不断被覆盖,并且堆中存储 a 的位置丢失了。这个内存没有被释放,因为程序没有退出。这称为“内存泄漏”。最终,你会用完堆上的所有内存,程序就会崩溃。

      【讨论】:

        【解决方案7】:

        堆由代码管理:删除堆分配是通过调用堆管理器来完成的。堆栈由硬件管理。没有经理可以打电话。

        【讨论】:

          猜你喜欢
          • 2012-02-20
          • 2017-12-02
          • 1970-01-01
          • 2018-06-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-08-14
          相关资源
          最近更新 更多