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


160

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

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

如果使用此垃圾收集器无法完成操作,那么您可以自己处理内存吗?

有人对我说了这个声明:

int * asafe=new int;

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

asafe=new int;


前段时间有一个非常类似的问题:堆栈和堆在哪里?这个问题有一些非常好的答案,应该可以让您有所了解。
斯科特·萨德

Answers:


223

有人问了类似的问题,但没有问静态问题。

什么是静态,堆和堆栈内存的摘要:

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

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

  • 解释堆栈的增长和收缩方式超出了这个答案的范围,但是足以说明您总是只从头开始添加和删除。堆栈通常从高位开始,然后下降到低位地址。当堆栈在中间某个地方遇到动态分配器时,内存用完了(但指的是物理内存还是虚拟内存以及碎片)。多个线程将需要多个堆栈(该进程通常为堆栈保留最小大小)。

当您想使用每一个时:

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

  • 当您知道只要函数在作用域内(在堆栈中的某个地方)时,您将希望保留变量,则堆栈变量很有用。堆栈非常适合您所需要的代码所在的变量,但在该代码之外并不需要。当您访问诸如文件之类的资源时,它们也非常有用,并且当您离开该代码时,希望资源自动消失。

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

垃圾收集

最近,我听到了很多关于垃圾收集器多么出色的信息,所以也许有些反对意见会有所帮助。

当性能不是一个大问题时,垃圾收集是一种很棒的机制。我听说GC越来越好,越来越复杂,但是事实是,您可能被迫接受性能损失(取决于用例)。而且,如果您很懒惰,它仍然可能无法正常工作。在最佳时机,垃圾收集器意识到没有更多引用后,您的内存就会消失(请参阅引用计数))。但是,如果您有一个引用自身的对象(可能是通过引用另一个引用该对象的对象),则仅引用计数将不会指示可以删除该内存。在这种情况下,GC需要查看整个参考汤,并找出是否存在仅由他们自己引用的岛。副手,我想这是一个O(n ^ 2)运算,但是无论如何,如果您完全关心性能,它可能会变得很糟糕。(编辑:Martin B 指出,对于相当有效的算法,它是O(n)。如果您担心性能,并且仍然可以在不垃圾回收的情况下进行恒定分配,那么它仍然是O(n)。)

就个人而言,当我听到人们说C ++没有垃圾收集时,我的想法将其标记为C ++的功能,但我可能只是少数。对于人们而言,学习C和C ++编程最困难的事情可能是指针以及如何正确处理其动态内存分配。没有GC,其他一些语言(例如Python)将变得可怕,因此我认为这取决于您想要的语言。如果您想要可靠的性能,那么我可以想到的是,没有垃圾收集的C ++是Fortran的这一方面中唯一的事情。如果您想要易于使用和训练轮子(为了避免崩溃而无需学习“适当的”内存管理),请使用GC选择一些东西。即使您知道如何很好地管理内存,也可以节省时间,您可以花时间优化其他代码。确实并没有太多的性能损失,但是如果您确实需要可靠的性能(以及能够准确地知道发生了什么情况,何时,在幕后的能力),那么我会坚持使用C ++。我听说过的每个主要游戏引擎都使用C ++(如果不是C或汇编语言)是有原因的。Python等人适合脚本编写,但不是主要的游戏引擎。


它与原始问题并没有真正的关系(或者实际上根本没有关系),但是您得到了堆栈的位置并向后堆积。 通常情况下,堆栈变小,堆变大(尽管堆实际上并没有“增长”,所以这是一个极大的简化)……
P Daddy

我不认为这个问题与另一个问题相似或重复。这是专门关于C ++的,他的意思几乎可以肯定是C ++中存在的三个存储期限。您可以在静态内存上分配动态对象,例如,过载op new。
Johannes Schaub-litb

7
您对垃圾收集的贬损处理没有帮助。
P爸爸2009年

9
如今,垃圾回收通常比手动释放内存更好,这是因为垃圾回收是在几乎没有工作要做的情况下发生的,而不是在可以使用性能的情况下释放内存。
格奥尔格Schölly09年

3
只是一小段评论-垃圾回收没有O(n ^ 2)复杂度(的确会对性能造成灾难性的影响)。一个垃圾收集周期所花费的时间与堆的大小成正比-请参阅hpl.hp.com/personal/Hans_Boehm/gc/complexity.html
马丁B

54

以下内容当然不是很精确。阅读时请带一点盐:)

好吧,您所指的三件事是自动的,静态的和动态的存储持续时间,这与对象的生存时间以及开始生存的时间有关。


自动储存时间

您将自动存储持续时间用于短寿命数据,这仅在某些块中本地需要:

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它并不绑定到类的特定对象,而是绑定到类本身。上面所有三个名称的地址是相同的,并且都表示相同的对象。关于何时以及如何初始化静态对象有特殊的规则,但是现在就不用担心了。这就是术语静态初始化顺序fiasco


动态存储期限

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

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本身具有自动存储期限。因此,它的生存期是自动管理的,从而使其可以检查是否应在其析构函数中删除指向动态对象。有关shared_ptr的参考,请参见增强文档:http : //www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm


39

有人说得很详细,就像“简短答案”:

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

  • 静态变量(全局作用域)
    生存期=程序运行时(1)
    可见性=在其中实例化的编译单元(2)

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

  • 堆栈变量
    可见性=从声明直到退出作用域
    生命周期=从声明直到退出作用域


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

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


5

我敢肯定,其中一名学徒很快就会提出更好的答案,但是主要区别在于速度和尺寸。

叠放

分配速度更快。它是在O(1)中完成的,因为它是在设置堆栈帧时分配的,因此它实际上是空闲的。缺点是,如果堆栈空间用完了,您就会陷入困境。您可以调整堆栈大小,但是IIRC可以使用〜2MB。同样,一旦退出函数,堆栈上的所有内容都会被清除。因此,以后再引用它可能会遇到问题。(指向堆栈分配的对象的指针会导致错误。)

分配速度明显慢。但是您有GB可以使用并指向。

垃圾收集器

垃圾收集器是一些在后台运行并释放内存的代码。当您在堆上分配内存时,很容易忘记释放它,这被称为内存泄漏。随着时间的流逝,您的应用程序消耗的内存会不断增长,直到崩溃。定期使垃圾回收器释放不再需要的内存有助于消除此类错误。当然,这是有代价的,因为垃圾收集器会使事情变慢。


3

静态和堆栈有什么问题?

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

在“堆栈”上进行分配的问题在于,执行分配的子例程一旦返回,分配就会被销毁。

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

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

什么是垃圾收集器?

它会一直监视您的数据(“标记并清除”),以检测您的应用程序何时不再引用它。这对于应用程序很方便,因为应用程序不需要取消分配数据...但是垃圾收集器可能在计算上很昂贵。

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

如果使用此垃圾收集器无法完成操作,那么您可以自己处理内存吗?

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

  • “静态”:永不释放
  • “堆栈”:变量“超出范围”
  • 'heap':删除指针时(由应用程序明确删除,或在某个或其他子例程中隐式删除)

1

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

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


1

如果您的程序不预先知道要分配多少内存,该怎么办(因此无法使用堆栈变量)。说链接列表,列表可以增长而无需预先知道它的大小。因此,当您不知道要向其中插入多少个元素时,对链表进行分配很有意义。


0

在某些情况下,GC的优点是在其他情况下会带来麻烦。对GC的依赖会鼓励您不要考虑太多。从理论上讲,要等到“空闲”期或绝对必须等待时,它才会占用带宽并导致应用程序响应延迟。

但是您不必“不用考虑”。就像多线程应用程序中的所有其他内容一样,只要可以屈服,就可以屈服。因此,例如,在.Net中,可以请求GC。通过这样做,您可以使运行频率较短的GC更为频繁,而不是降低运行频率较长的GC的利用率,并分散与此开销相关的延迟。

但这与GC的主要吸引力背道而驰,GC的主要吸引力似乎是“鼓励人们不必去考虑太多,因为它是自动的”。

如果您是在GC普及之前才开始编程的,并且对malloc / free和new / delete感到满意,那么您甚至可能发现GC有点烦人和/或不信任(因为人们可能不信任'最佳化”的历史。)许多应用程序可以容忍随机延迟。但是对于不接受随机延迟的应用程序,常见的反应是避开GC环境并朝着完全不受管理的代码的方向发展(或者禁止使用长久以来垂死的技术和汇编语言)。

不久前,我在这里有一个暑期学生,一个实习生,聪明的孩子,已经断奶了。他对GC的优越性非常满意,以至于即使在非托管C / C ++中进行编程时,他也拒绝遵循malloc / free new / delete模型,因为引用“您不必使用现代编程语言来做到这一点”。而且你知道?对于运行时间短的微型应用程序,您确实可以避免,但对于性能长期运行的应用程序则无法解决。


0

堆栈是由编译器分配的内存,每当我们编译程序时,默认情况下编译器都会从OS分配一些内存(我们可以从IDE中的编译器设置更改设置),而OS是为您分配内存的内存,具体取决于在系统上的许多可用内存以及许多其他东西上,当我们声明一个变量时,要分配给堆栈内存,这些变量将它们复制(引用为形式),这些变量被压入堆栈,默认情况下它们遵循一些命名约定,它们在Visual Studio中遵循CDECL例如:中缀符号:c = a + b;堆栈推入操作从右向左进行,b用于堆栈,运算符,a用于堆栈以及i,ec的结果进行堆栈。以前缀表示法:= + cab在此,所有变量都被推入堆栈1st(从右至左),然后进行操作。编译器分配的内存是固定的。因此,假设为我们的应用程序分配了1MB的内存,假设变量使用了700kb的内存(除非所有本地变量都被动态分配,否则它们将被压入堆栈),因此剩余的324kb内存将分配给堆。而且,该堆栈的生命周期较短,当函数范围结束时,这些堆栈将被清除。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.