为什么不能在堆栈上释放变量?

Why can't you free variables on the stack?

本文关键字:释放 变量 堆栈 不能 为什么      更新时间:2023-10-16

有问题的语言是C/C++。

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

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

我知道编译器释放了堆栈上的变量,但这是在变量权利范围的末尾。它不是也在其作用域结束时释放堆上的变量吗?如果没有,为什么不呢?

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

(指针通常分配给变量,但这无济于事。

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

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

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

{
    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在它们之前被弹出。因此,每次编写"}"符号时,您都在有效地缩小堆栈并"释放"数据。

已经有很好的答案,但我认为您可能需要更多澄清,所以我会尝试使这个答案更详细,并尝试使其简单(如果我设法的话)。如果有些不清楚(因为我的母语不是英语,有时可能在制定答案时遇到问题),请在评论中询问。还将使用Kerrek SB在他的答案中使用的变量对象的想法。

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

堆栈上的变量得到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是一个变量(因为它有自己的名称),这意味着它也是一个对象

现在堆呢?让我们将您可能认为的"堆上的东西"描述为指向堆上某个内存位置的变量,该变量指向对象所在的堆上的某个内存位置。现在这些东西得到了所谓的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. 从您的评论中:是的,我明白了,但这是在变量权利范围的末尾。它不是也在其作用域结束时释放堆上的变量吗?

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

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

    好吧,计算机程序并不总是只有几int。让我用一点"假"引用来回答:

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

    但这还不够,我们都应该知道。多少内存才足够?我不知道,但肯定不是我们现在得到的。有许多算法,问题和其他东西需要大量内存。想想电脑游戏之类的东西。如果我们有更多的内存,我们能做出"更大"的游戏吗?想想就知道了...你总是可以用更多的资源做更大的事情,所以我认为没有任何限制,我们可以说这已经足够了。

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

"堆栈分配器"知道的所有信息都是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

阅读有关函数调用的信息 - 每次调用都会在堆栈上推送数据和函数地址。函数从堆栈中弹出数据并最终推送其结果。

通常,堆栈由操作系统管理,是的 - 它可能会被耗尽。只需尝试执行以下操作:

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

这种情况应该会很快结束。

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

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

的值不断被覆盖,堆中存储 A 的位置丢失。此内存未释放,因为程序不会退出。这称为"内存泄漏"。最终,您将耗尽堆上的所有内存,程序将崩溃。

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