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

Stack, Static, and Heap in C++

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

我听说其他语言包含一个"垃圾收集器",所以您不必担心内存问题。垃圾收集器做什么?

使用这个垃圾回收器,你能自己操作内存吗?

有一次有人对我说这句话:

1
int * asafe=new int;

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

1
asafe=new int;


有人问了一个类似的问题,但没有问静态的问题。好的。静态内存、堆内存和堆栈内存的摘要:

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

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

  • 解释堆栈的增长和收缩方式有点超出了这个答案的范围,但足以说明您总是只从末尾添加和删除。堆栈通常从高位开始,然后向下扩展到低位地址。当堆栈在中间某个地方遇到动态分配器时,内存将耗尽(但请参阅物理内存与虚拟内存和碎片)。多线程将需要多个堆栈(进程通常为堆栈保留最小大小)。好的。

当您想使用每一个时:

  • 静态/全局对您知道永远需要的内存很有用,而且您知道永远不想取消分配。(顺便说一下,嵌入式环境可能被认为只有静态内存…堆栈和堆是由第三种内存类型(程序代码)共享的已知地址空间的一部分。当程序需要链表之类的东西时,它们通常会从静态内存中进行动态分配。但是,不管怎样,静态内存本身(缓冲区)本身并不是"分配的",而是从缓冲区为此而持有的内存中分配其他对象。您也可以在非嵌入式环境中这样做,并且控制台游戏经常会避开内置的动态内存机制,从而通过使用所有分配的预设大小的缓冲区来严格控制分配过程。)好的。

  • 当您知道只要函数在作用域内(在某个堆栈上),您就希望变量保持不变时,堆栈变量非常有用。栈对于代码所需的变量是很好的,但在代码之外不需要。当您访问一个资源(如文件)并希望在您离开代码时该资源自动消失时,它们也非常适合。好的。

  • 头分配(动态分配存储器)是当你想比上面更灵活时使用的。通常,一个函数被称为响应事件(用户点击"创建盒"按钮)。The proper response may need allocating a new object(a new box object)that should stick around long after the function is existed,so it can't be on the stack.但你不知道你想要多少拳击手在方案开始时,所以它不是静态的。

    okay.

垃圾收集

我最近听说了很多关于大男孩收藏家的故事所以也许是一个失败的声音的比特

okay.

垃圾收集是一种神奇的机制,当性能不是一个问题。我听说的GCS是更好和更复杂的,但事实是,你可能被迫接受一个性能罚款(取决于使用情况)。如果你是Lazy,也许还没开始工作At the best of times,garbage collectors realize that your memory goes away when it realizes that there are no more references to it(see reference counting).但是,如果你有一个对象,参照它自己(可能是参照另一个对象,后面参照的对象),那么,参照数本身并不表示可以删除记忆。在这一案例中,如果有任何岛屿是它们唯一提到的,则GC需要仔细检查整个参照汤和图表。离开这里,我想做一个O(N ^ 2)操作,但不管是什么,如果你是一个完全与性能有关的人,它会变得不好。(edition:Martin B points out that it is O(n)for reasonably efficient algorithms.如果你对性能感兴趣,而且能在不含垃圾收集的情况下在恒定时间内进行交易,这仍然是太多了。)

okay.

就个人而言,当我听到人们说C++没有垃圾收集的时候,我的脑海里把它标记为C++的一个特性,但我可能是少数。在C和C++中,人们最难了解的编程是指针,以及如何正确地处理它们的动态内存分配。其他一些语言,比如python,如果没有gc的话会很糟糕,所以我认为归根结底,这取决于语言的需求。如果你想要可靠的性能,那么C++中没有垃圾回收是我能想到的FORTRAN的唯一一个方面。如果你想要易于使用和训练轮子(为了避免崩溃而不需要你学习"正确的"内存管理),用gc选择一些东西。即使您知道如何很好地管理内存,它也将为您节省优化其他代码的时间。实际上没有太多的性能损失了,但是如果你真的需要可靠的性能(并且能够准确地知道发生了什么,当在封面下),那么我会坚持C++。有一个原因,我听说过的每一个主要游戏引擎都是C++(如果不是C或汇编)。python等人可以编写脚本,但不是主要的游戏引擎。好的。好啊。


当然,以下内容并不十分准确。当你读到它的时候,用一粒盐把它带走:)

嗯,你提到的三件事是自动的、静态的和动态的存储时间,这与物体的寿命以及它们开始生命的时间有关。

自动存储时间

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

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

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

静态存储时间

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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,它在函数体被输入时发生。对于命名空间范围内的对象,生存期从程序启动时开始。类作用域的静态对象也是如此:

1
2
3
4
5
6
7
class A {
    static string classScopeA;
};

string A::classScopeA;

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

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

动态存储持续时间

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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时,它的生存期才会结束。如果忘记了这一点,这些对象永远不会结束生命。定义用户声明的构造函数的类对象不会调用其析构函数。具有动态存储持续时间的对象需要手动处理其生存期和关联的内存资源。图书馆的存在是为了方便使用它们。可以使用智能指针为特定对象建立显式垃圾收集:

1
2
3
4
5
6
7
8
int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

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

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


正如"简短的回答"一样,人们精心地说:

  • 静态变量(类)生存期=程序运行时(1)可见性=由访问修饰符(private/protected/public)确定

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

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

  • 栈变量可见性=从声明到退出范围生存期=从声明到声明范围退出

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

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


我相信其中一个学生很快就会想出一个更好的答案,但主要的区别是速度和大小。

分配速度显著加快。它是在O(1)中完成的,因为它是在设置堆栈帧时分配的,所以它基本上是空闲的。缺点是,如果堆栈空间用完了,那么就成了骨骼。您可以调整堆栈大小,但iIRC有~2MB可供使用。此外,一旦退出函数,堆栈上的所有内容都将被清除。所以以后再参考它可能会有问题。(指向堆栈分配对象的指针会导致错误。)

分配速度明显减慢。但是你有国标可以玩,并且指向。

垃圾收集器

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


What are the problems of static and stack?

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

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

I could write an entire application without allocate variables in the heap?

也许,但不是一个非平凡的,正常的,大的应用程序(但所谓的"嵌入式"程序可以编写没有堆,使用一个子集的C++)。

What garbage collector does ?

它一直监视您的数据("标记和扫描"),以检测您的应用程序何时不再引用它。这对应用程序很方便,因为应用程序不需要解除分配数据…但是垃圾收集器的计算成本可能很高。

垃圾收集器不是C++编程的常见特征。

What could you do manipulating the memory by yourself that you couldn't do using this garbage collector?

学习用于确定性内存分配的C++机制:

  • "静态":从不释放
  • "stack":一旦变量"超出范围"
  • "heap":删除指针时(由应用程序显式删除,或在某些或其他子例程中隐式删除)

在某些情况下,GC的一个优点是在另一些情况下会带来麻烦;对GC的依赖鼓励人们不要多想它。理论上,等待直到"空闲"期,或者直到它绝对必须等待时,它将窃取带宽并在应用程序中造成响应延迟。

但你不必"不去想它",就像多线程应用程序中的其他所有东西一样,当你可以让步时,你可以让步。例如,在.NET中,可以请求一个GC;通过这样做,您可以拥有更频繁、更短的GC运行,而不是更短的GC运行频率,并分散与此开销相关的延迟。

但这一点击败了气相色谱的主要吸引力,气相色谱似乎"被鼓励不用多想,因为它是自动材料集成电路"。

如果你在gc流行之前就开始接触编程,并且对malloc/free和new/delete感到满意,那么你甚至可能会觉得gc有点烦人和/或不信任(因为人们可能不信任"优化",它有着复杂的历史)。许多应用程序都能容忍随机延迟。但是对于那些不那么容易接受随机延迟的应用程序来说,一个常见的反应是避开GC环境,朝着纯非托管代码的方向移动(或者说,上帝保佑,这是一种正在消亡的艺术,汇编语言)。

我有一个夏天的学生在这里回来,一个实习生,一个聪明的孩子,他在GC上断奶了。他对GC的优越性非常敬佩,即使在非托管C/C++中编程时,他拒绝遵循MalC/Cuff/Neal/Delphi模型,因为引用了"你不应该用现代编程语言做这件事",你知道吗?对于小型、短时间运行的应用程序,您确实可以从中解脱出来,但对于长时间运行的高性能应用程序则不行。


如果程序不预先知道要分配多少内存(因此不能使用堆栈变量),该怎么办?比如说链接列表,列表可以在不预先知道其大小的情况下增长。因此,当您不知道将有多少元素插入到一个堆中时,对一个链接列表进行分配是有意义的。


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

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


堆栈是由编译器分配的内存,当我们编译程序时,默认情况下编译器从OS分配一些内存(我们可以从您的IDE中的编译器设置中更改设置),OS是给您内存的一个,它依赖于系统上的许多可用内存和许多其他东西,进入堆栈内存是分配的。当我们声明一个变量时,它们会复制(引用为formls),这些变量被推到堆栈上,默认情况下,它们在Visual Studio中遵循一些命名约定。例如:中缀符号:c=a+b;堆栈推送由右向左推送、B向堆栈、运算符、A向堆栈以及I、E、C向堆栈的结果。在预固定符号中:=+CAB这里所有变量都被推到第一个堆栈(从右到左),然后进行操作。编译器分配的内存是固定的。所以假设将1MB的内存分配给我们的应用程序,假设变量使用了700KB的内存(除非动态分配,否则所有本地变量都被推到堆栈中),所以剩余的324KB内存分配给堆。当函数的作用域结束时,这个堆栈的生命周期就变短了,这些堆栈就被清除了。