为什么c++没有垃圾收集器?

我问这个问题,首先不是因为垃圾收集的优点。我问这个问题的主要原因是我知道Bjarne Stroustrup说过c++在某个时间点上会有一个垃圾收集器。

话虽如此,为什么还没有加入呢?c++已经有了一些垃圾收集器。这是那种“说起来容易做起来难”的事情吗?或者还有其他原因没有添加它(并且不会在c++ 11中添加)?

交叉链接:

澄清一下,我理解c++最初创建时没有垃圾收集器的原因。我想知道为什么不能添加收集器。

202788 次浏览
< p >什么类型?它应该针对嵌入式洗衣机控制器、手机、工作站或超级计算机进行优化吗?< br > 它应该优先考虑gui响应还是服务器加载?< br > 它应该使用大量内存还是大量CPU?< / p > C/c++在太多不同的环境中使用。 我怀疑像boost智能指针之类的东西对大多数用户来说已经足够了

编辑-自动垃圾收集器不是一个性能问题(你总是可以买更多的服务器),这是一个可预测的性能问题 不知道GC什么时候会起作用就像雇佣一个嗜睡症的飞行员,大多数时候他们是很棒的-但当你真的需要响应的时候!< / p >

本来可以添加隐式垃圾收集,但它没有达到要求。可能不仅仅是因为实现的复杂性,还因为人们不能足够快地达成普遍共识。

引用Bjarne Stroustrup自己的话:

我曾希望一个垃圾收集器 哪些可以选择启用 是c++ 0x的一部分,但是 我有足够多的技术问题 凑合着用一个详细的 如何规范这样的收集器 的其余部分集成 语言,如果提供的话。事实就是这样 基本上拥有c++ 0x的所有特性, 存在一个实验性的实现

关于在这里这个话题有一个很好的讨论。

总体概述:

c++非常强大,几乎可以做任何事情。由于这个原因,它不会自动将许多可能影响性能的东西推给您。垃圾收集可以很容易地用智能指针(用引用计数包装指针的对象,当引用计数达到0时自动删除自己)实现。

c++在构建时考虑到了没有垃圾收集功能的竞争对手。与C和其他语言相比,效率是c++必须抵御批评的主要问题。

垃圾收集有两种类型…

显式垃圾收集:

c++ 0x通过shared_ptr创建的指针进行垃圾收集

如果你想要它,你可以使用它,如果你不想要它,你不会被迫使用它。

对于c++ 0x之前的版本,boost:shared_ptr是存在的,用途相同。

隐式垃圾收集:

但是它没有透明的垃圾收集。不过,它将是未来c++规范的一个焦点。

为什么Tr1没有隐式垃圾收集?

c++ 0x的tr1应该有很多东西,Bjarne Stroustrup在之前的采访中说tr1没有他想要的那么多。

c++背后的思想是,你不需要为你不使用的特性付出任何性能上的影响。因此,添加垃圾收集意味着让一些程序像C语言那样直接在硬件上运行,而另一些则在某种运行时虚拟机中运行。

没有什么可以阻止您使用绑定到某些第三方垃圾收集机制的某种形式的智能指针。我似乎记得微软在COM上做过类似的事情,但并不顺利。

如果你想要自动垃圾收集,有很好的商业 和c++的公共域垃圾收集器。适用于以下应用程序 适合垃圾收集,c++是一个优秀的垃圾收集 一种性能优于其他垃圾的语言 收集的语言。参见 c++编程语言(第4 用于c++中自动垃圾收集的讨论。 看,汉斯- j。Boehm's 站点用于C和c++垃圾收集 (存档).

此外,c++支持允许内存的编程技术 管理为安全且隐式,没有垃圾收集器。我认为垃圾收集是资源管理的最后选择,也是一种不完美的处理方式。这并不意味着它永远都没用,只是在许多情况下有更好的方法。< / p >

来源:http://www.stroustrup.com/bs_faq.html#garbage-collection

至于为什么它没有内置它,如果我没记错的话,它是在GC是事情之前发明的,我不相信该语言可能有GC,有几个原因(我。C)向后兼容。

希望这能有所帮助。

要回答大多数关于c++的“为什么”问题,请阅读c++的设计与发展

c++没有内置垃圾回收的最大原因之一是,让垃圾回收很好地使用析构函数是非常非常困难的。据我所知,还没有人真正知道如何完全解决这个问题。有很多问题需要处理:

  • 对象的确定性生命周期(引用计数提供了这一点,但GC没有。虽然这可能不是什么大问题)。
  • 如果在垃圾收集对象时抛出析构函数会发生什么?大多数语言都会忽略这个异常,因为实际上没有catch块可以传输它,但这对于c++来说可能不是一个可接受的解决方案。
  • 如何启用/禁用它?当然,这可能是一个编译时的决定,但为GC编写的代码与为非GC编写的代码将是非常不同的,可能是不兼容的。你如何调和这两者?

这些只是面临的问题中的一小部分。

为了增加争论。

关于垃圾收集有一些已知的问题,了解它们有助于理解为什么c++中没有垃圾收集。

1. 性能?

第一个抱怨通常是关于性能,但大多数人并没有真正意识到他们在谈论什么。正如Martin Beckett所说明的那样,问题可能不是性能本身,而是性能的可预测性。

目前有两个GC家族被广泛部署:

  • 标记和清扫类
  • 引用计数类型

Mark And Sweep更快(对整体性能的影响较小),但它患有“冻结世界”综合症:即当GC开始时,所有其他事情都将停止,直到GC完成清理。如果您希望构建一个在几毫秒内响应的服务器……有些交易不会达到你的期望:)

Reference Counting的问题是不同的:引用计数会增加开销,特别是在多线程环境中,因为你需要原子计数。此外,还有参考循环的问题,所以你需要一个聪明的算法来检测这些循环并消除它们(通常也通过“冻结世界”来实现,尽管不太常见)。一般来说,到今天为止,这种类型(即使通常反应更灵敏,或者更确切地说,冻结更少)比Mark And Sweep慢。

我看过一篇Eiffel实现者的论文,他们试图实现一个Reference Counting垃圾收集器,它将具有与Mark And Sweep类似的全局性能,但没有“冻结世界”方面。它需要一个单独的GC线程(典型的)。算法(在最后)有点吓人,但论文很好地一次介绍了一个概念,并展示了算法从“简单”版本到成熟版本的演变。推荐阅读,如果我能把我的手放回PDF文件…

2. 资源获取初始化(RAII)

C++中,将资源的所有权包装在对象中,以确保它们被正确释放,这是一个常见的习语。它主要用于内存,因为我们没有垃圾回收,但它对许多其他情况也很有用:

  • 锁(多线程,文件句柄,…)
  • 连接(到数据库、另一台服务器……)

其思想是正确地控制对象的生命周期:

  • 只要你需要,它就应该是活的
  • 当你用完它的时候,它应该被杀死

GC的问题在于,如果它有助于前者,并最终保证以后……这个“终极”可能还不够。如果你释放一个锁,你真的希望它现在被释放,这样它就不会阻止任何进一步的调用!

带有GC的语言有两种解决方法:

  • 当堆栈分配足够时不要使用GC:这通常是为了解决性能问题,但在我们的例子中,它确实有帮助,因为作用域定义了生命周期
  • using构造……但它是显式(弱)RAII,而在c++中RAII是隐式的,因此用户不会在不知不觉中犯错误(通过省略using关键字)

3.智能指针

C++中,智能指针通常作为处理内存的银弹出现。我经常听到:我们根本不需要GC,因为我们有智能指针。

这是大错特错了。

智能指针确实有帮助:auto_ptrunique_ptr使用RAII概念,确实非常有用。它们很简单,你可以很容易地自己写出来。

然而,当一个人需要共享所有权时,它变得更加困难:你可能在多个线程之间共享,并且在计数的处理上有一些微妙的问题。因此,我们自然会选择shared_ptr

这很棒,毕竟这就是Boost的用途,但它并不是万能的。事实上,shared_ptr的主要问题是它模拟了由Reference Counting实现的GC,但你需要自己实现周期检测…开始

当然有这个weak_ptr的东西,但不幸的是,尽管使用了shared_ptr,但我已经看到了内存泄漏,因为这些循环…当你在多线程环境中,它是非常难以检测的!

4. 解决方案是什么?

没有什么灵丹妙药,但一如既往,这绝对是可行的。在没有GC的情况下,需要明确所有权:

  • 如果可能的话,最好在一个特定的时间拥有一个所有者
  • 如果没有,确保你的类图没有任何与所有权相关的循环,并通过微妙的weak_ptr应用来打破它们

所以,如果有一个GC…然而,这不是一个微不足道的问题。与此同时,我们只需要卷起袖子。

虽然这是一个问题,但仍然有一个问题我没有看到任何人解决:垃圾收集几乎不可能指定。

特别是,c++标准非常谨慎地根据外部可观察到的行为来指定语言,而不是实现如何实现该行为。然而,在垃圾收集的情况下,实际上没有外部可观察到的行为。

垃圾收集的一般的想法是它应该做出合理的尝试,以确保内存分配成功。不幸的是,本质上不可能保证任何内存分配都会成功,即使您确实在运行垃圾收集器。在某种程度上,这在任何情况下都是正确的,但在c++中尤其如此,因为它(可能)不可能使用复制收集器(或任何类似的)在收集周期中将对象移动到内存中。

如果不能移动对象,就不能创建一个单独的、连续的内存空间来进行分配——这意味着您的堆(或自由存储区,或任何您喜欢称呼它的地方)可能会随着时间的推移而变得碎片化。这反过来又会阻止分配成功,即使空闲内存比请求的内存多。

虽然可能会提出一些保证,它说(本质上)如果你重复完全相同的分配模式,并且它第一次成功,它将在后续迭代中继续成功,前提是分配的内存在迭代之间变得不可访问。这是一个非常微弱的保证,基本上毫无用处,但我看不到任何加强它的合理希望。

即便如此,它也比为c++所提议的更强大。之前的提议[警告:PDF](被删除)根本不能保证任何东西。在28页的提案中,你在外部可观察到的行为中看到的是一个单一的(不规范的)注释:

[注意:对于垃圾收集程序,高质量的托管实现应该尝试最大限度地回收不可访问的内存量。-结束注释]

至少对我来说,这提出了一个关于投资回报的严重的问题。我们将破坏现有的代码(没有人知道具体破坏了多少,但肯定是相当多),对实现提出新的要求,对代码提出新的限制,而我们得到的回报很可能是什么都没有?

即使在最好的情况下,我们得到的是基于Java测试的程序,以相同的速度运行可能需要大约6倍的内存。更糟糕的是,垃圾收集从一开始就是Java的一部分——c++对垃圾收集器施加了足够多的限制,以至于它几乎肯定会有一个甚至更糟糕的是的成本/收益比(即使我们超出了提案所保证的范围,并假设会有一些收益)。

我要用数学方法总结一下情况:这是一个复杂的情况。数学家都知道,复数有两部分:实数和虚数。在我看来,我们这里的成本是真实的,但收益(至少大部分)是虚构的。

所有的技术讨论都使这个概念过于复杂。

如果您将GC自动放入c++以获取所有内存,那么可以考虑使用类似web浏览器的东西。web浏览器必须加载完整的web文档并运行web脚本。web脚本变量可以存储在文档树中。在打开大量选项卡的浏览器中的大文档中,这意味着每次GC必须进行完整收集时,它还必须扫描所有文档元素。

在大多数计算机上,这意味着会发生PAGE错误。所以回答这个问题的主要原因是PAGE错误会发生。当你的电脑开始进行大量的磁盘访问时,你就会知道这一点。这是因为GC必须使用大量内存才能证明无效指针。当您有一个使用大量内存的真正的应用程序时,由于PAGE错误,必须扫描每个集合的所有对象是严重的。页面错误是指虚拟内存需要从磁盘读入RAM。

因此,正确的解决方案是将应用程序分为需要GC的部分和不需要GC的部分。在上面的web浏览器示例中,如果文档树是用malloc分配的,但javascript是用GC运行的,那么每次GC启动时,它只扫描一小部分内存,并且文档树的内存中所有page OUT元素不需要被换回。

为了进一步理解这个问题,请查阅虚拟内存以及它是如何在计算机中实现的。这都是关于当没有那么多RAM时,程序可用2GB的事实。在32BIt系统的2GB RAM的现代计算机上,只要只有一个程序在运行,就不存在这样的问题。

作为另一个示例,考虑一个必须跟踪所有对象的完整集合。首先,您必须扫描所有通过根访问的对象。第二步扫描步骤1中可见的所有对象。然后扫描等待的析构函数。然后再次浏览所有页面,关闭所有不可见对象。这意味着许多页面可能会被多次换出和换回。

因此,我的回答是,由于触及所有内存而发生的PAGE FAULTS的数量导致对程序中所有对象进行完整的GC是不可行的,因此程序员必须将GC视为脚本和数据库工作的辅助,但使用手动内存管理进行正常工作。

另一个很重要的原因当然是全局变量。为了让收集器知道全局变量指针在GC中,它需要特定的关键字,因此现有的c++代码将无法工作。

< p >简短的回答: 我们不知道如何有效地(在很小的时间和空间开销下)并在所有可能的情况下(在所有可能的情况下)正确地进行垃圾收集 < p >长答: 就像C一样,c++是一种系统语言;这意味着当您编写系统代码时,例如操作系统时,将使用它。换句话说,c++的设计就像C一样,以最好的性能作为主要目标。语言标准不会增加任何可能阻碍性能目标的特性

这暂停了这个问题:为什么垃圾收集会影响性能?主要原因是,当涉及到实现时,我们(计算机科学家)不知道如何在所有情况下以最小的开销进行垃圾收集。因此,c++编译器和运行时系统不可能一直有效地执行垃圾收集。另一方面,c++程序员应该了解他的设计/实现,他是决定如何最好地进行垃圾收集的最佳人选。

最后,如果控制(硬件、细节等)和性能(时间、空间、电源等)不是主要的限制,那么c++就不是合适的工具。其他语言可能会更好,并提供更多[隐藏的]运行时管理,以及必要的开销。

Stroustrup在2013年的Going Native大会上对此发表了一些很好的评论。

只需要跳到这个视频中约25m50s即可。(其实我建议你看完整个视频,但这段视频跳过了垃圾收集的内容。)

当你有一种非常棒的语言,可以很容易(而且安全、可预测、易于阅读和易于教授)直接处理对象和值,避免(显式)使用堆,那么你甚至不需要想要垃圾收集。

在现代c++和c++ 11中,垃圾收集不再需要,除非在有限的情况下。事实上,即使一个好的垃圾收集器内置于一个主要的c++编译器中,我认为它也不会经常使用。它将是更容易,而不是更难,以避免GC。

他举了一个例子:

void f(int n, int x) {
Gadget *p = new Gadget{n};
if(x<100) throw SomeException{};
if(x<200) return;
delete p;
}

这在c++中是不安全的。但在Java中也是不安全的!在c++中,如果函数提前返回,delete将永远不会被调用。但是如果你有完整的垃圾收集,比如在Java中,你只会得到一个建议,对象将在“未来的某个时候”被销毁(更新:更糟糕。Java 承诺永远调用终结器——它可能永远不会被调用)。如果Gadget拥有一个打开的文件句柄,或者一个到数据库的连接,或者您缓冲了以便稍后写入数据库的数据,那么这就不够好了。我们希望小工具在完成后立即被销毁,以便尽快释放这些资源。您不希望数据库服务器在数千个不再需要的数据库连接中挣扎——它不知道您的程序已经完成工作。

那么解决方案是什么呢?有几种方法。最明显的方法,你将用于绝大多数的对象是:

void f(int n, int x) {
Gadget p = {n};  // Just leave it on the stack (where it belongs!)
if(x<100) throw SomeException{};
if(x<200) return;
}

这需要更少的字符来输入。它没有new的阻碍。它不需要你输入Gadget两次。对象在函数结束时被销毁。如果这是你想要的,这是非常直观的。__abc1的行为与intdouble相同。可预测,易读,易教。一切都是“价值”。有时是一个很大的值,但是值更容易教,因为你不需要指针(或引用)那样的“远距离操作”。

您创建的大多数对象仅用于创建它们的函数,并且可能作为输入传递给子函数。程序员不应该在返回对象时考虑“内存管理”,或者在软件的广泛分离部分之间共享对象。

范围和生命周期很重要。大多数情况下,如果生命期与作用域相同,就会更容易。这样更容易理解,也更容易教。当你想要一个不同的生存期时,阅读你正在做这件事的代码应该很明显,例如通过使用shared_ptr。(或利用move-semantics或unique_ptr按值返回(大)对象。

这似乎是一个效率问题。如果我想从foo()返回一个Gadget怎么办?c++ 11的move语义使得返回大对象更容易。只要写Gadget foo() { ... },它就会工作,而且很快。你不需要自己摆弄&&,只要按值返回就行了,语言通常能够做必要的优化。(即使在c++ 03之前,编译器在避免不必要的复制方面做得非常好。)

正如Stroustrup在视频的其他地方所说的(释义):“只有计算机科学家才会坚持复制一个物体,然后破坏原件。(观众笑)。为什么不直接将对象移动到新位置呢?这是人类(而不是计算机科学家)所期望的。”

当您可以保证只需要一个对象的副本时,就更容易理解对象的生命周期。您可以选择您想要的生命周期策略,如果您愿意,垃圾收集就在那里。但是,当您了解其他方法的好处时,您会发现垃圾收集在首选列表的底部。

如果这对你不起作用,你可以使用unique_ptr,或者失败的话,使用shared_ptr。在内存管理方面,编写良好的c++ 11比许多其他语言更短,更容易阅读,也更容易教授。

当我们比较c++和Java时,我们看到c++在设计时并没有考虑到隐式垃圾收集,而Java则是。

在C风格中使用任意指针这样的东西不仅不利于gc实现,而且还会破坏大量c++遗留代码的向后兼容性。

除此之外,c++是一种旨在作为独立可执行文件运行的语言,而不是具有复杂的运行时环境。

总而言之: 是的,在c++中添加垃圾收集是可能的,但是为了连续性,最好不要这样做

原始C语言背后的一个基本原则是,内存是由一系列字节组成的,代码只需要关心这些字节在被使用的确切时刻意味着什么。现代C语言允许编译器施加额外的限制,但C语言包括——c++保留了——将指针分解为字节序列,将包含相同值的任何字节序列组装为指针,然后使用该指针访问先前的对象。

虽然这种能力在某些类型的应用程序中是有用的——甚至是不可或缺的——但包含这种能力的语言在支持任何有用和可靠的垃圾收集方面的能力将非常有限。如果编译器不知道对组成指针的比特所做的所有操作,它将无法知道是否在宇宙的某个地方存在足以重建指针的信息。由于这些信息可能以计算机即使知道它们也无法访问的方式存储(例如,组成指针的字节可能已经在屏幕上显示了足够长的时间,以至于有人将它们写在一张纸上),因此计算机可能不可能知道一个指针是否可能在未来被使用。

许多垃圾收集框架的一个有趣的特点是,对象引用不是由其中包含的位模式定义的,而是由对象引用中包含的位与其他地方包含的其他信息之间的关系定义的。在C和c++中,如果存储在指针中的位模式标识了一个对象,则该位模式将标识该对象,直到显式销毁该对象。在典型的GC系统中,对象在某一时刻可能由位模式0x1234ABCD表示,但下一个GC周期可能将对0x1234ABCD的所有引用替换为对0x4321BABE的引用,因此对象将由后一种模式表示。即使要显示与对象引用相关的位模式,然后从键盘上读取它,也不能期望相同的位模式可用于识别相同的对象(或任何对象)。

主要有两个原因:

  1. 因为它不需要(恕我直言)
  2. 因为它与RAII几乎不兼容,RAII是c++的基石

c++已经提供了手动内存管理、堆栈分配、RAII、容器、自动指针、智能指针……这应该足够了。垃圾收集器适合懒惰的程序员,他们不想花5分钟思考谁应该拥有哪些对象或什么时候应该释放资源。这不是我们在c++中做事情的方式。

tl;dr:因为现代c++不需要垃圾收集。

Bjarne Stroustrup的FAQ 对这件事的回答说:

我不喜欢垃圾。我不喜欢乱扔垃圾。我的理想是通过不产生任何垃圾来消除对垃圾收集器的需求。现在这是可能的。


这些天编写的代码(c++ 17和遵循官方核心指导方针)的情况如下:

  • 大多数与内存所有权相关的代码都在库中(特别是那些提供容器的库)。
  • 大多数涉及内存所有权的代码使用遵循CADRe或RAII 模式,因此在构造时进行分配,在销毁时进行回收,这发生在退出分配的范围时。
  • 不显式分配或释放内存直接
  • 原始指针没有自己的记忆(如果你遵循了指导方针),所以你不能通过传递它们来泄漏。
  • 如果你想知道如何在内存中传递值序列的起始地址——你可以而且应该选择跨度,从而避免了对原始指针的需要。你仍然可以使用这样的指针,只是它们是非所有的。
  • 如果你真的需要一个拥有的“指针”,你可以使用c++ ` 标准库智能指针 -它们不会泄漏,而且相当高效(尽管英国保险协会可能会碍事 of that)。或者,你可以使用“主人pointers"跨范围边界传递所有权。这些是不常见的,必须明确使用;但是当被采用时,它们允许对泄漏进行很好的静态检查。

“哦,是吗?但是……

... 如果我只是像以前写c++那样写代码?”

实际上,你可以只是忽略了所有的指导方针,并编写了泄漏的应用程序代码-它将编译和运行(并泄漏),一如既往。

但这不是“不要那样做”。在这种情况下,开发者需要保持良好的道德,并进行大量的自我控制;编写不符合规范的代码并不简单,也没有更快,也没有更好的性能。渐渐地,它也会变得越来越难写,因为你会面临越来越多的“阻抗不匹配”。符合规范所提供和期望的。

…如果我reintrepret_cast?或者做复杂的指针运算?还是其他类似的伎俩?”

事实上,如果你用心去做,你可以编写一些代码,尽管你很好地遵循了指导原则。但是:

  1. 您很少会这样做(就代码中的位置而言,而不一定是就执行时间的比例而言)
  2. 你只会故意这么做,而不是意外。
  3. 这样做将在符合准则的代码库中脱颖而出。
  4. 在这种代码中,您无论如何都可以在另一种语言中绕过GC。

... 图书馆发展?“

如果你是c++库开发人员,那么你确实会编写包含原始指针的不安全代码,并且你被要求谨慎而负责地编码——但这些是由专家编写的自包含代码片段(更重要的是,由专家评审)。


所以,就像Bjarne说的:一般来说,收集垃圾真的没有动力,因为你都是为了确保不产生垃圾。GC正在成为c++的一个不成问题的问题。

这并不是说,当您希望使用自定义分配和反分配策略时,GC对于某些特定的应用程序不是一个有趣的问题。对于那些您想要自定义分配和反分配的对象,而不是语言级GC。

强制垃圾收集实际上是一个低级到高级的范式转换。

如果你观察带有垃圾收集的语言中处理字符串的方式,你会发现它们只允许高级字符串操作函数,不允许对字符串进行二进制访问。简单地说,所有字符串函数都首先检查指针以查看字符串的位置,即使您只是绘制一个字节。因此,如果您正在使用带有垃圾收集的语言执行一个循环,处理字符串中的每个字节,它必须计算每次迭代的基位置加上偏移量,因为它不知道字符串什么时候移动了。然后你必须考虑堆、栈、线程等等。