堆栈,静态和堆在C + +

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

我听说其他语言包含一个“垃圾收集器”,所以你不必担心内存。 垃圾收集器是做什么的?

你可以自己操纵记忆,你不能使用这个垃圾回收器吗?

有人对我说,有了这个声明:

int * asafe=new int; 

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

 asafe=new int; 

一个类似的问题被问到,但它没有问静态。

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

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

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

  • 解释一个堆栈如何增长和缩小是有点超出这个答案的范围,但只要说你总是只添加和删除。 堆栈通常从高处开始,然后长到低地址。 当堆栈满足中间某个地方的动态分配器(但是指物理内存和虚拟内存以及碎片)时,内存不足。 多个线程将需要多个堆栈(该过程通常为该堆栈保留最小的大小)。

当你想要使用每一个:

  • 静态/全局对于你知道永远需要的内存是有用的,而且你知道你永远不会想要释放内存。 (顺便说一句,嵌入式环境可能被认为只有静态存储器…堆栈和堆是第三种存储器类型共享的已知地址空间的一部分:程序代码。程序通常会做动态分配静态内存,当他们需要像链表这样的东西时,但不管怎样,静态内存本身(缓冲区)本身不是“分配”的,而是其他对象被分配出缓冲区保存的内存。在非嵌入式的情况下,控制台游戏将经常避开内置的动态内存机制,有利于通过对所有分配使用预设大小的缓冲区来严格控制分配过程。

  • 当你知道只要函数在范围内(在堆栈上),堆栈变量是有用的,你会希望变量保持不变。 堆栈对于你所需的代码所在的变量是很好的,但是在代码之外并不需要这些变量。 当你访问一个资源时,它们也是非常好的,比如一个文件,并且当你离开这个代码的时候,资源会自动消失。

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

垃圾收集

最近我听到很多关于垃圾收集器的情况,所以也许有一点不同意的声音会有帮助。

垃圾收集是一个很好的机制,当性能不是一个大问题。 我听说GC正在变得越来越好,但事实是,您可能会被迫接受性能处罚(取决于使用情况)。 如果你懒,它仍然可能无法正常工作。 在最好的时候,垃圾收集器意识到,当你意识到没有更多的引用时,你的记忆会消失(参见引用计数 )。 但是,如果您有一个引用自身的对象(可能通过引用另一个引用的对象),则单独引用计数不会指示可以删除内存。 在这种情况下,GC需要查看整个参考汤,并确定是否有任何岛屿只能由他们自己提及。 不管怎样,我想这是一个O(n ^ 2)的操作,但不管它是什么,如果你全心全意地关注性能,它可能会变坏。 (编辑:Martin B 指出合理有效的算法是O(n),如果你关心性能,那么它仍然是O(n),并且可以在没有垃圾回收的情况下在不变的情况下释放。

就个人而言,当我听到人们说C ++没有垃圾回收功能时,我的头脑就把它作为C ++的一个功能,但我可能只是少数。 对于人们学习C和C ++编程最困难的事情是指针,以及如何正确处理动态内存分配。 一些其他的语言,比如Python,在没有GC的情况下会很糟糕,所以我认为这取决于你想要什么语言。 如果你想要可靠的性能,那么没有垃圾回收的C ++是我能想到的Fortran的这一面。 如果你想要使用方便和训练轮(为了避免碰撞而不要求你学习“正确”的内存管理),用GC选择一些东西。 即使你懂得如何管理内存,也可以节省你花时间优化其他代码。 再也没有太多的性能损失,但是如果你真的需要可靠的性能(以及能够知道到底发生了什么,在什么时候,在什么时候),那么我会坚持使用C ++。 有一个原因,我曾经听说过的每个主要的游戏引擎都是用C ++(如果不是C或汇编)。 Python等对于脚本编程来说不错,但不是主要的游戏引擎。

以下当然都不是很精确。 当你读它时,用一粒盐把它拿走:)

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


自动存储时间

短期数据使用自动存储持续时间,仅在某些块内本地需要:

 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(); } 

只有当你打电话给他们删除时,它的生命周期才会结束 如果你忘记了,那些对象永远不会终结。 定义一个用户声明的构造函数的类对象将不会调用它的析构函数。 具有动态存储持续时间的对象需要手动处理其使用期限和相关的内存资源 存在的图书馆,以方便使用它们。 通过使用智能指针可以建立特定对象的 显式垃圾收集

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

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

有人说得很详细,就像“简答”一样:

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

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

  • 堆变量
    生命周期=由您定义(新的删除)
    可见性=由你定义(无论你指定的指针)

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


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

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

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

显着更快分配。 它在O(1)中完成,因为它是在设置堆栈帧时分配的,因此它基本上是空闲的。 缺点是,如果你用完堆栈空间,你是骨架。 您可以调整堆栈大小,但IIRC你有〜2MB玩。 而且,只要你退出函数,堆栈中的所有东西都被清除。 所以稍后再提到它可能会有问题。 (指向堆栈分配的对象导致错误。)

分配明显较慢。 但是你有GB玩,并指向。

垃圾收集器

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

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

“静态”分配的问题是分配是在编译时进行的:不能用它来分配一些可变数量的数据,其数量在运行时间之前是未知的。

在“堆栈”上分配的问题是,只要分配的子例程返回,分配就会被销毁。

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

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

什么垃圾收集器呢?

它一直在观察你的数据(“标记和扫描”)来检测你的应用程序何时不再引用它。 这对于应用程序来说很方便,因为应用程序不需要释放数据…但垃圾收集器的计算成本可能很高。

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

你可以自己操纵记忆,你不能使用这个垃圾回收器吗?

学习确定性内存释放的C ++机制:

  • “静态”:从不释放
  • '堆栈':只要变量“超出范围”
  • 'heap':当指针被删除(被应用程序明确删除,或者在某个子程序中被隐式删除)

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

你可以很容易地泄漏内存而不需要垃圾收集器,但是你也可以决定什么时候释放对象和内存。 我运行GC时遇到了Java问题,而且我有一个实时进程,因为GC是一个独占线程(没有别的东西可以运行)。 所以如果性能是关键的,你可以保证没有泄漏的对象,不使用GC是非常有用的。 否则,当你的应用程序消耗内存并且必须追踪泄漏的来源时,它才会使你讨厌生活。

如果你的程序不知道多少内存分配(因此你不能使用堆栈变量)。 说链接列表,列表可以不预先知道它的大小。 因此,当你不知道有多少元素会被插入时,在一个堆上进行分配对于链表是有意义的。

GC在某些情况下的一个优势是其他方面的烦恼, 对GC的依赖鼓励不要太多考虑。 理论上,等到“闲置”期间,或者直到它绝对必须,当它将窃取带宽并导致您的应用程序的响应延迟。

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

但是这打败了GC的主要吸引力,似乎“被鼓励不必多想,因为它是自动化的”。

如果你在GC开始流行之前首先接触到程序,并且对malloc / free和new / delete感到满意,那么你甚至可能会觉得GC有点烦人和/或不信任(因为人们可能不太相信“优化“,已经有一个方格的历史。)许多应用程序容忍随机延迟。 但对于那些不适用的应用程序来说,随机延迟不太可以接受,常见的反应是避开GC环境,向纯粹非托管的代码(或上帝禁止,长期垂死的艺术,汇编语言)的方向发展。

我有一个暑假的学生,一个实习生,聪明的孩子,在GC上断奶。 他对GC的高层如此ad that,即使在非托管C / C ++编程时,他也拒绝遵循malloc / free新/删除模式,因为引用“你不应该用现代编程语言来做这件事”。 而且你知道? 对于小型,短期运行的应用程序,你确实可以逃避这一点,但不是长期运行的应用程序。

Stack是由编译器分配的内存,当我们编译程序时,默认情况下编译器会从OS分配一些内存(我们可以在IDE中更改编译器设置的设置),OS是给你内存的那个,它依赖于在系统上的许多可用内存和许多其他事物上,并且当我们声明一个变量被复制时,堆栈内存被分配(参考形式),这些变量被推入堆栈,它们遵循一些命名约定,默认情况下它的CDECL在Visual Studio例如:中缀符号:c = a + b; 堆栈推进是左到右推,堆栈,操作员,堆栈和结果的那些我,EC堆栈。 在前缀表示法中:= + cab这里所有的变量都被压入堆栈1(从右到左),然后进行操作。 这个由编译器分配的内存是固定的。 所以我们假设1MB的内存分配给我们的应用程序,让变量使用700kb的内存(所有的本地变量都被推到堆栈,除非它们是动态分配的),所以剩余的324kb内存被分配给堆。 而且这个堆栈的使用寿命更短,当函数的作用域结束时,这些堆栈被清除。