首页 文章

C中的堆栈,静态和堆

提问于
浏览
142

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

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

您可以自己操作内存,使用垃圾收集器无法做到这一点?

有人告诉我这个声明:

int * asafe=new int;

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

asafe=new int;

9 回答

  • 205

    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编程最困难的可能是指针以及如何正确处理动态内存分配 . 如果没有GC,其他一些语言(如Python)会很糟糕,所以我认为这取决于你想要的语言 . 如果你想要可靠的性能,那么没有垃圾收集的C是我能想到的Fortran这一方面唯一的东西 . 如果您想要易于使用和训练轮(为了避免崩溃而不需要您学习“适当的”内存管理),请使用GC选择一些东西 . 即使您知道如何妥善管理内存,也可以节省您优化其他代码的时间 . 实际上没有太多的性能损失,但如果你真的需要可靠的性能(并且能够准确地知道发生了什么,何时,在幕后)那么我会坚持使用C.有一个原因,我听说过的每个主要游戏引擎都在C(如果不是C或汇编) . Python等人可以编写脚本,但不是主要的游戏引擎 .

  • 0

    以下当然都不太准确 . 读到它时带上一粒盐:)

    嗯,你提到的三件事是 automatic, static and dynamic storage duration ,它与物体的生存时间和生命时间有关 .


    自动存储持续时间

    您对 short livedsmall 数据使用自动存储持续时间,在某些块中仅需要 locally

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

    一旦我们退出块,生命周期就会结束,并且一旦定义了对象就会立即开始 . 它们是最简单的存储持续时间,并且比特定的动态存储持续时间更快 .


    静态存储持续时间

    您对自由变量使用静态存储持续时间,任何代码都可以访问它们,如果它们的范围允许这样的使用(命名空间范围),以及需要在其范围的退出(本地范围)上延长其生命周期的局部变量,以及对于需要由其类的所有对象(类范围)共享的成员变量 . 它们的生命周期取决于它们所处的范围 . 它们可以具有 namespace scopelocal scope 以及 class scope . 这两个人的真实情况是,一旦他们的生命开始,生命终止于 the end of the program . 以下是两个例子:

    // 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 在其块退出时不会被销毁 . 您可以说具有本地范围的对象开始生命周期 when control reaches their definition . 对于 localA ,它会在输入函数体时发生 . 对于命名空间范围内的对象,生命周期从 program startup 开始 . 对于类范围的静态对象也是如此:

    class A {
        static string classScopeA;
    };
    
    string A::classScopeA;
    
    A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;
    

    如您所见, classScopeA 并未绑定到其类的特定对象,而是绑定到类本身 . 上面所有三个名称的地址都是相同的,都表示相同的对象 . 有关何时以及如何初始化静态对象的特殊规则,但让's not concern about that now. That'表示静态初始化顺序fiasco .


    动态存储持续时间

    最后的存储持续时间是动态的 . 如果你想让对象在另一个岛上生存,你想使用它,并且你想要引用它们引用它们 . 如果对象是 big ,并且如果要创建仅在 runtime 处已知的大小数组,也可以使用它们 . 由于这种灵活性,具有动态存储持续时间的对象复杂且管理缓慢 . 具有该动态持续时间的对象在发生适当的新操作符调用时开始生命周期:

    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时才会结束 . 如果你忘记了,那些对象永远不会终结 . 定义用户声明的构造函数的类对象不会调用它们的析构函数 . 具有动态存储持续时间的对象需要手动处理其生存期和相关的存储器资源存在库以便于使用它们 . Explicit garbage collection for particular objects 可以使用智能指针 Build :

    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

  • 51

    精心设计,就像“简短回答”:

    • static variable (class)
      lifetime =程序运行时(1)
      visibility =由访问修饰符(private / protected / public)确定

    • static variable (global scope)
      lifetime =程序运行时(1)
      visibility =在(2)中实例化的编译单元

    • heap variable
      lifetime =由您定义(新删除)
      visibility =您定义的(无论您指定指针的是什么)

    • stack variable
      visibility =从声明到退出范围
      lifetime =从声明直到声明范围退出


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

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

  • 36

    我相信其中一个学生会很快得到一个更好的答案,但主要区别在于速度和体型 .

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

    分配速度极慢 . 但你有GB可以玩,并指向 .

    垃圾收集器

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

  • 5

    静态和堆栈有什么问题?

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

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

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

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

    什么垃圾收集器呢?

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

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

    你可以自己操作内存,你不能使用这个垃圾收集器吗?

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

    • 'static':从未取消分配

    • 'stack':变量"goes out of scope"

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

  • 1

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

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

  • 3

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

  • 1

    GC在某些情况下的优势在于其他情况下的烦恼;对GC的依赖鼓励人们不要多考虑它 . 理论上,等到“空闲”期间或直到绝对必须,它会窃取带宽并导致应用程序中的响应延迟 .

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

    但这打败了GC的主要吸引力,似乎“鼓励他们不必多思考它,因为它是自动化的 . ”

    如果您在GC变得普遍并且感到舒适之前首次接触到编程使用malloc / free和new / delete,甚至可能会发现GC有点恼人和/或不信任(因为人们可能不信任'优化',这已经有了一段曲折的历史 . )许多应用程序容忍随机延迟 . 但对于没有随机延迟不太可接受的应用程序,常见的反应是避开GC环境并朝着完全不受管理的代码(或上帝禁止,长期垂死的艺术,汇编语言)的方向前进 .

    我有一个夏天的学生在这里,一个实习生,聪明的孩子,谁在GC断奶;他非常喜欢GC的优势,即使在非托管C / C编程时他拒绝遵循malloc / free new / delete模型,因为引用“你不应该用现代编程语言来做这件事 . ”而且你知道?对于小型,短期运行的应用程序,您确实可以逃脱,但不适用于长期运行的高性能应用程序 .

  • 1

    堆栈是由编译器分配的内存,当我们编译程序时,默认编译器从OS分配一些内存(我们可以从IDE中的编译器设置更改设置),OS是给你内存的,它取决于在系统上的许多可用内存和许多其他东西上,当我们声明一个他们复制的变量(ref as formals)时会分配到堆栈内存,这些变量被推送到堆栈它们遵循一些命名约定默认情况下它在Visual Studio中的CDECL ex:中缀符号:c = ab;堆栈推送从右到左进行推送,b进行堆栈,操作,a到堆栈以及那些i的结果,即堆叠 . 在预修复符号中:= cab这里所有变量都被推送到堆栈1(从右到左),然后进行操作 . 由编译器分配的内存是固定的 . 因此,假设我们的应用程序分配了1MB的内存,假设变量使用了700kb的内存(除非动态分配,否则所有局部变量都被推送到堆栈中),因此剩余的324kb内存被分配给堆 . 并且这个堆栈的生命周期较短,当函数的范围结束时,这些堆栈被清除 .

相关问题