有问题的语言是 C/C++。
我的教授说在你使用完堆后释放内存,否则你可能会得到无法访问的内存。这样做的问题是您最终可能会用完所有内存,并且您无法访问其中的任何内容。
为什么相同的概念不适用于堆栈?我知道你总是可以访问你在堆栈上使用的内存,但是如果你不断创建新变量,你最终会用完空间对吗?那么为什么不能像在堆上一样释放堆栈上的变量来为新变量腾出空间呢?
我知道编译器释放堆栈上的变量,但那是在变量范围的末尾。它不会在其作用域的末尾释放堆上的变量吗?如果不是,为什么不呢?
有问题的语言是 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 ,它是动态“字符串”的包装器。
现在澄清你的一些问题:
为什么我们不能销毁堆栈上的对象?
简单的回答:你为什么想要?
详细回答:它会被你销毁,一旦离开不允许的范围,它就会再次销毁。此外,您通常应该只在您的范围内拥有计算实际需要的变量,如果您确实需要该变量来完成计算,您将如何销毁它?但是,如果你真的只需要一个变量在计算中的一小段时间内,你可以创建一个新的更小的范围,{ }
这样你的变量就会在不再需要时自动销毁。
注意:如果您有很多变量,而您只需要一小部分计算,则可能暗示该部分计算应该在其自己的函数/范围内。
从您的评论中:是的,我明白了,但那是在变量范围的末尾。它不会在其作用域的末尾释放堆上的变量吗?
他们没有。堆上的对象没有作用域,您可以将它们的地址从函数中传递出来,它仍然存在。指向它的指针可能会超出范围并被销毁,但堆上的 Object 仍然存在,您无法再访问它(内存泄漏)。这也是它被称为手动内存管理的原因,大多数人更喜欢围绕它们的包装器,以便在不再需要时自动销毁它。请参阅 std::string、std::vector 作为示例。
根据您的评论:另外,您如何在计算机上耗尽内存?一个 int 占用 4 个字节,大多数计算机都有数十亿字节的内存......(不包括嵌入式系统)?
好吧,计算机程序并不总是只保留几个int
s。让我用一点“假”的报价来回答:
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 的位置丢失了。这个内存没有被释放,因为程序没有退出。这称为“内存泄漏”。最终,你会用完堆上的所有内存,程序就会崩溃。