在(非垃圾收集)C++ 中,这个想法是内存管理是通过析构函数完成的,而不是一些垃圾收集器。这样做的主要优点是:
(1) 非常精确。该语言对何时以及以何种顺序调用析构函数以及释放内存做出了非常有力的保证。
(2) 它简单、透明且完全可移植,而且开销真正最小。与其依赖于一些不透明的垃圾收集机制,它可能会随心所欲地做事,而是确切地知道内存管理将如何在您编译的每个系统上工作。如果你不确定什么时候/如果东西被释放,你可以把调试输出放在析构函数中并检查它。确实,现代垃圾收集器非常好,而且很少引起问题,但是如果您真的遇到过问题并且需要调试一个,您就会知道这可能非常耗时且痛苦。
我知道堆内存和堆栈内存之间存在对性能有影响的区别,但是我对这个主题并不了解。
堆和堆栈正是它们在 C 中的样子。堆栈的基本思想是,它是一块内存,用于存储程序的函数调用堆栈。堆栈被布置为一系列“堆栈帧”。当一个函数被调用时,一个指针被推送,它告诉我们稍后返回时应该返回到哪里。函数调用的所有参数都连续放置在堆栈上。还有一个计数器/指针指示此堆栈帧有多大。当一个函数被调用时,一个新的栈帧继续,当函数返回时我们弹出它。函数的局部变量也分配在堆栈上。堆栈具有固定大小,如果超过它(通常通过无限递归),则会因堆栈溢出错误而崩溃。堆栈上的所有内容都必须具有在编译时已知的固定大小。这是由“sizeof”运算符确定的。如果你想拥有一个动态数组,你必须使用“堆”,它允许在大小仅在运行时知道时分配内存,并且可以以任意顺序破坏事物。
堆栈的优点是内存分配基本上是即时的(堆栈帧指针刚刚移动)并且以非常舒适的方式进行释放。(当我们从函数返回并且本地变量超出范围时,我们只需从堆栈中弹出以释放它们的内存。)C++ 对堆栈分配的对象和析构函数做出了非常强的保证。保证将始终以与创建相反的顺序在函数返回时调用析构函数,即使它抛出异常而不是正常返回也是如此。本质上,除非您的程序异常终止(std::terminate()
,或throw 42
),或者除非发生一些不寻常的事情(在一些特殊情况下,你的析构函数本身会抛出异常,但基本上你不应该这样做,因为它会自找麻烦),自动对象的析构函数将在非常特殊的点被调用时间,以非常特殊的顺序。如果它们具有带有构造函数/析构函数的成员变量,那么它们也会以特定顺序被调用。如果涉及继承......如果您不熟悉,您可以阅读它。
基本上,这是一种非常强大和严格的机制,您可以使用它来控制各种资源——不仅仅是内存,还有诸如套接字、到打印机的连接、指向必须“关闭”的 C 库实例的指针等任何东西独占的共享资源,或者在不再需要时必须以某种方式清理的资源。
不是所有的东西都可以入栈。例如,如果你有一个动态大小的数组,它不能放在堆栈上,它必须放在堆上,这与 C 中通常教授的方式相同。(因为只有编译器知道大小的东西才能进入堆栈。)当你把东西放在堆上时,(在 C++ 中使用 operator new )它必须与 delete 调用配对,否则内存不会被释放并且析构函数不会被调用。
在 C++ 中,管理它的首选方法是利用堆栈的力量。您无需自己手动调用 delete,而是使用其析构函数调用 delete 的堆栈分配对象。最简单的例子是“std::unique_ptr”(又名 boost::scoped_ptr)。你提到的 shared_ptr 也做了类似的事情。所有的 C++ 标准容器,如 vector、list、map 等也都这样做。
IMO 这是模式基本上是 C++ 的福音。在编写良好的 C++ 程序中,不仅所有内存,而且所有资源都以这种方式管理,由析构函数释放。这是非常重要的RAII习语:“Resource Acquisition is Initialization”。你可以在这里阅读更多关于它的信息:http ://www.c2.com/cgi/wiki?ResourceAcquisitionIsInitialization
在许多程序中,所有对象都只有一个所有者,对象所有权图是一棵树。在这种情况下,一切都可以堆栈分配/堆栈分配的成员变量,然后所有内存都打开并由堆栈管理。在某些情况下,对象需要共享所有权——所有权图是一个 DAG(有向无环图)。然后您可以将 shared_ptr 用于共享对象。它们将在堆上分配,但它会自动工作并且内存将被管理,而无需您做更多的事情。在最复杂的情况下,所有权图中存在循环引用、循环。如果您在这种情况下仅使用 shared_ptr ,则不会释放循环。这就是垃圾收集器应该为你“做艰苦的工作”的情况。在现代 C++ 中,首选的方法是在大多数地方仍然使用 shared_ptr,但在任何循环中至少有一个链接,您使用 weak_ptr 代替,这样循环“被破坏”并且可以自动释放。这很少是必要的,当它是,它真的没有那么多工作。它只是许多其他语言的自动垃圾收集的另一种工程风格。