ChatGPT解决这个技术问题 Extra ChatGPT

C++ 中的堆栈、静态和堆

我搜索过,但我对这三个概念不是很了解。我什么时候必须使用动态分配(在堆中),它的真正优势是什么?静态和堆栈的问题是什么?我可以编写整个应用程序而不在堆中分配变量吗?

我听说其他语言包含“垃圾收集器”,因此您不必担心内存。垃圾收集器是做什么的?

你能做些什么来自己操作内存,而使用这个垃圾收集器是做不到的?

曾经有人用这个宣言对我说:

int * asafe=new int;

我有一个“指向指针的指针”。这是什么意思?它是不同的:

asafe=new int;

?

前段时间有人问过一个非常相似的问题:What and where are the stack and heap?该问题有几个 really good 答案,应该可以对您的问题有所启发。

C
Community

A similar question 被问到,但它没有询问静力学。

什么是静态、堆和堆栈内存的总结:

静态变量基本上是全局变量,即使您无法全局访问它。通常在可执行文件本身中有一个地址。整个程序只有一个副本。无论您进入函数调用(或类)多少次(以及在多少个线程中!),该变量都指向相同的内存位置。

堆是一堆可以动态使用的内存。如果你想要一个 4kb 的对象,那么动态分配器将查看它在堆中的可用空间列表,挑选出一个 4kb 的块,然后给你。通常,动态内存分配器(malloc、new 等)从内存末尾开始并向后工作。

解释堆栈如何增长和缩小有点超出了这个答案的范围,但只要说你总是从最后添加和删除就足够了。堆栈通常从高位开始并向下增长到较低的地址。当堆栈在中间某处遇到动态分配器时,您会耗尽内存(但指的是物理内存与虚拟内存和碎片)。多个线程将需要多个堆栈(进程通常为堆栈保留最小大小)。

当您想使用每一个时:

静态/全局对于您知道您将始终需要并且您知道您永远不想释放的内存很有用。 (顺便说一句,嵌入式环境可能被认为只有静态内存......堆栈和堆是由第三种内存类型共享的已知地址空间的一部分:程序代码。程序通常会从它们的当他们需要链表之类的东西时使用静态内存.但无论如何,静态内存本身(缓冲区)本身并不是“分配的”,而是其他对象为此目的从缓冲区持有的内存中分配出来.你可以这样做在非嵌入式中也是如此,主机游戏会经常避开内置的动态内存机制,转而使用预设大小的缓冲区来严格控制分配过程。)

当您知道只要函数在范围内(在某处的堆栈上),您会希望变量保留时,堆栈变量很有用。堆栈非常适合您所在的代码所需的变量,但在该代码之外不需要。当您访问资源(如文件)并希望资源在您离开该代码时自动消失时,它们也非常有用。

当您想要比上述更灵活时,堆分配(动态分配的内存)很有用。通常,调用函数来响应事件(用户单击“创建框”按钮)。正确的响应可能需要分配一个新对象(一个新的 Box 对象),该对象在函数退出后应该保留很长时间,因此它不能在堆栈上。但是你不知道在程序开始时你想要多少个盒子,所以它不能是静态的。

垃圾收集

我最近听到很多关于垃圾收集器有多棒的消息,所以也许有点反对的声音会有所帮助。

当性能不是一个大问题时,垃圾收集是一种很好的机制。我听说 GC 越来越好,越来越复杂,但事实是,您可能被迫接受性能损失(取决于用例)。如果你很懒惰,它仍然可能无法正常工作。在最好的情况下,垃圾收集器会意识到当它意识到不再有对它的引用时,你的记忆就会消失(参见 reference counting)。但是,如果您有一个引用自身的对象(可能通过引用另一个引用回来的对象),那么单独的引用计数并不能表明可以删除内存。在这种情况下,GC 需要查看整个参考汤,并确定是否存在任何仅由它们自己参考的岛。顺便说一句,我猜这是一个 O(n^2) 操作,但不管它是什么,如果你完全关心性能,它可能会变得很糟糕。 (编辑:Martin B points out,对于相当有效的算法来说,它是 O(n)。如果您关心性能并且可以在没有垃圾收集的情况下在恒定时间内解除分配,那仍然是 O(n) 太多。)

就我个人而言,当我听到人们说 C++ 没有垃圾收集时,我认为这是 C++ 的一个特性,但我可能是少数。人们学习 C 和 C++ 编程最难的事情可能是指针以及如何正确处理它们的动态内存分配。其他一些语言,比如 Python,如果没有 GC,会很糟糕,所以我认为这归结为你想要从一种语言中得到什么。如果你想要可靠的性能,那么没有垃圾收集的 C++ 是我能想到的 Fortran 这边唯一的东西。如果您想要易于使用和训练轮子(以使您免于崩溃,而无需您学习“正确的”内存管理),请选择带有 GC 的东西。即使您知道如何很好地管理内存,它也会节省您可以花在优化其他代码上的时间。真的没有太多的性能损失了,但是如果你真的需要可靠的性能(并且能够准确地知道发生了什么,什么时候,在幕后),那么我会坚持使用 C++。我听说过的每个主要游戏引擎都使用 C++(如果不是 C 或汇编),这是有原因的。 Python 等适用于脚本编写,但不适用于主游戏引擎。


它与原始问题并不真正相关(或者实际上根本不相关),但是您得到了堆栈和堆的位置。通常,堆栈会向下增长而堆会增长(尽管堆实际上并没有“增长”,所以这是一个巨大的过度简化)......
我不认为这个问题与另一个问题相似甚至重复。这个是专门关于 C++ 的,他的意思几乎可以肯定是 C++ 中存在的三个存储持续时间。您可以在静态内存上分配一个动态对象就好了,例如,重载 op new。
现在,垃圾收集通常比手动释放内存更好,因为它发生在几乎没有工作要做的时候,而不是释放内存,而释放内存则可以在性能可以用于其他方面时发生。
@gs:有趣的一点。当然,您可以使用非 GC 懒惰地解除分配,因此它再次归结为易用性与微观管理的能力。如果易用性让您有时间在其他地方进行优化,那么这是一个很好的性能增益。我会调整。
只是一个小评论——垃圾收集没有 O(n^2) 复杂性(这确实会对性能造成灾难性的影响)。一个垃圾回收周期所用的时间与堆的大小成正比——参见hpl.hp.com/personal/Hans_Boehm/gc/complexity.html
b
bolov

以下当然不是很精确。当你阅读它时,请带着一粒盐:)

嗯,你说的三件事是自动的、静态的和动态的存储时长,这与对象的寿命以及它们何时开始寿命有关。

自动存储时间

您对短期和小数据使用自动存储持续时间,这仅在某个块内本地需要:

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

生命周期在我们退出块后立即结束,并在对象定义后立即开始。它们是最简单的存储持续时间,并且比特定的动态存储持续时间快得多。

静态存储时间

您对自由变量使用静态存储持续时间,如果它们的范围允许这样使用(命名空间范围),任何代码都可以随时访问这些自由变量,以及需要在退出其范围(本地范围)时延长其生命周期的局部变量,以及对于需要由其类的所有对象(类范围)共享的成员变量。它们的生命周期取决于它们所处的范围。它们可以具有命名空间范围、本地范围和类范围。他们俩的真实情况是,一旦他们的生命开始,生命就会在程序结束时结束。这里有两个例子:

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

程序打印 ababab,因为 localA 在其块退出时没有被销毁。您可以说具有本地范围的对象在控制达到其定义时开始生命周期。对于 localA,它在输入函数的主体时发生。对于命名空间范围内的对象,生命周期从程序启动开始。对于类范围的静态对象也是如此:

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

如您所见,classScopeA 未绑定到其类的特定对象,而是绑定到类本身。上面三个名字的地址都是一样的,都表示同一个对象。关于何时以及如何初始化静态对象有一些特殊的规则,但我们现在不用担心。这就是术语静态初始化顺序惨败的意思。

动态存储时长

最后的存储持续时间是动态的。如果您想让对象存在于另一个岛上,并且您想在该引用周围放置指针,则可以使用它。如果您的对象很大,并且如果您想创建仅在运行时知道大小的数组,您也可以使用它们。由于这种灵活性,具有动态存储持续时间的对象是复杂且管理缓慢的。当发生适当的新运算符调用时,具有该动态持续时间的对象开始生命周期:

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

只有当您为它们调用 delete 时,它的生命周期才会结束。如果您忘记了这一点,这些对象将永远不会结束生命周期。定义用户声明的构造函数的类对象不会调用它们的析构函数。具有动态存储持续时间的对象需要手动处理它们的生命周期和相关的内存资源。图书馆的存在是为了便于使用它们。可以使用智能指针为特定对象建立显式垃圾收集:

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

您不必关心调用 delete:如果引用该对象的最后一个指针超出范围,则共享 ptr 会为您执行此操作。共享 ptr 本身具有自动存储持续时间。所以 its 生命周期是自动管理的,允许它检查是否应该删除其析构函数中指向的动态对象。有关 shared_ptr 参考,请参阅 boost 文档:http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm


p
peterchen

就像“简短的回答”一样,它已经被详细地说出来了:

静态变量(类)生命周期 = 程序运行时 (1) 可见性 = 由访问修饰符确定(私有/受保护/公共)

静态变量(全局范围)生命周期 = 程序运行时 (1) 可见性 = 在 (2) 中实例化它的编译单元

堆变量生存期 = 由您定义(新删除)可见性 = 由您定义(无论您将指针分配给什么)

堆栈变量可见性=从声明到退出范围生命周期=从声明到退出声明范围

(1) 更准确地说:从编译单元(即C/C++文件)的初始化直到反初始化。标准未定义编译单元的初始化顺序。

(2) 注意:如果在头文件中实例化一个静态变量,每个编译单元都会得到自己的副本。


C
Chris Smith

我敢肯定其中一位学究很快会提出更好的答案,但主要区别在于速度和大小。

分配速度大大加快。它在 O(1) 中完成,因为它是在设置堆栈帧时分配的,因此它基本上是免费的。缺点是如果你用完了堆栈空间,你就会被束缚。您可以调整堆栈大小,但 IIRC 有大约 2MB 可供使用。此外,一旦您退出该函数,堆栈上的所有内容都会被清除。因此,稍后引用它可能会有问题。 (指向堆栈分配对象的指针会导致错误。)

分配速度要慢得多。但是你有GB可以玩,并指向。

垃圾收集器

垃圾收集器是一些在后台运行并释放内存的代码。当你在堆上分配内存时,很容易忘记释放它,这就是所谓的内存泄漏。随着时间的推移,您的应用程序消耗的内存会不断增长,直到崩溃。让垃圾收集器定期释放您不再需要的内存有助于消除此类错误。当然,这是有代价的,因为垃圾收集器会减慢速度。


C
ChrisW

静态和堆栈的问题是什么?

“静态”分配的问题在于分配是在编译时进行的:您不能使用它来分配一些可变数量的数据,这些数据的数量直到运行时才知道。

在“堆栈”上分配的问题是,一旦执行分配的子例程返回,分配就会被破坏。

我可以编写整个应用程序而不在堆中分配变量吗?

也许但不是一个不平凡的、正常的、大型应用程序(但所谓的“嵌入式”程序可能在没有堆的情况下使用 C++ 的子集编写)。

垃圾收集器做什么?

它会持续监视您的数据(“标记和扫描”)以检测您的应用程序何时不再引用它。这对应用程序来说很方便,因为应用程序不需要释放数据……但是垃圾收集器的计算成本可能很高。

垃圾收集器不是 C++ 编程的常见功能。

你能做些什么来自己操作内存,而使用这个垃圾收集器是做不到的?

了解确定性内存释放的 C++ 机制:

'静态':从不释放

'stack':一旦变量“超出范围”

'heap':当指针被删除时(由应用程序显式删除,或在某个或其他子例程中隐式删除)


R
Rob Elsner

当堆栈太“深”并且溢出可用于堆栈分配的内存时,堆栈内存分配(函数变量、局部变量)可能会出现问题。堆用于需要从多个线程或整个程序生命周期访问的对象。您可以在不使用堆的情况下编写整个程序。

如果没有垃圾收集器,您可以很容易地泄漏内存,但您也可以指定何时释放对象和内存。我在运行 GC 时遇到了 Java 的问题,并且我有一个实时进程,因为 GC 是一个独占线程(没有其他线程可以运行)。因此,如果性能至关重要并且您可以保证没有泄漏的对象,那么不使用 GC 是非常有帮助的。否则,当您的应用程序消耗内存并且您必须追踪泄漏源时,它只会让您讨厌生活。


k
kal

如果您的程序不知道要分配多少内存(因此您不能使用堆栈变量)怎么办。说链表,列表可以在不知道它的大小的情况下增长。因此,当您不知道要向其中插入多少元素时,在堆上分配对于链表来说是有意义的。


f
frediano

GC 在某些情况下的优势是在其他情况下令人烦恼。对 GC 的依赖鼓励不要过多考虑它。从理论上讲,等到“空闲”时间或绝对必须等到它会窃取带宽并导致您的应用程序响应延迟。

但你不必“不去想它”。就像多线程应用程序中的其他所有内容一样,当您可以让步时,您就可以让步。例如,在 .Net 中,可以请求 GC;通过这样做,您可以更频繁地运行较短的 GC,而不是较不频繁地运行较长时间的 GC,并分散与此开销相关的延迟。

但这违背了 GC 的主要吸引力,它似乎“鼓励不必考虑太多,因为它是自动的”。

如果您在 GC 流行之前第一次接触编程并且对 malloc/free 和 new/delete 感到满意,那么您甚至可能会发现 GC 有点烦人和/或不信任(因为有人可能不信任 '优化”,它有一个曲折的历史。)许多应用程序容忍随机延迟。但是对于那些随机延迟不太可接受的应用程序,一个常见的反应是避开 GC 环境并朝着纯非托管代码的方向发展(或者上帝保佑,一种长期垂死的艺术,汇编语言。)

前段时间我在这里有一个暑期学生,一个实习生,聪明的孩子,在 GC 上断了奶。他非常坚信 GC 的优越性,以至于即使在非托管 C/C++ 中进行编程时,他也拒绝遵循 malloc/free new/delete 模型,因为引用的话,“你不应该在现代编程语言中这样做。”而且你知道?对于小型、短时间运行的应用程序,您确实可以摆脱这种情况,但对于长时间运行的高性能应用程序则不行。


r
raj

堆栈是编译器分配的内存,当我们编译程序时,默认编译器会从 OS 分配一些内存(我们可以从 IDE 中的编译器设置中更改设置),而 OS 是为您提供内存的内存,这取决于在系统上的许多可用内存和许多其他事情上,当我们声明他们复制的变量(作为形式引用)时分配到堆栈内存,这些变量被推送到堆栈默认情况下它们遵循一些命名约定它在 Visual Studios 中的 CDECL例如:中缀表示法:c=a+b;堆栈推入是从右到左推入,b 到堆栈,运算符,a 到堆栈和那些 i,ec 到堆栈的结果。在前缀表示法中: =+cab 这里所有的变量都被推送到第一个堆栈(从右到左),然后进行操作。编译器分配的内存是固定的。因此,假设为我们的应用程序分配了 1MB 内存,假设变量使用了 700kb 内存(所有局部变量都被推送到堆栈,除非它们是动态分配的),所以剩余的 324kb 内存分配给堆。而且这个栈的生命周期更短,当函数的作用域结束时,这些栈就会被清除。