首先,我提出这个问题并不是因为垃圾回收的优点。我提问的主要原因是我知道Bjarne Stroustrup曾经说过C++在某个时间点会有一个垃圾回收器。
话虽如此,为什么还没有添加呢?已经有一些用于C++的垃圾回收器了。这只是那种“说起来容易做起来难”的事情吗?或者还有其他原因(并且在C++11中不会被添加)?
跨链接:
仅仅为了澄清一下,我理解C++在创立时没有垃圾回收器的原因。我想知道的是为什么不能添加回收器。
首先,我提出这个问题并不是因为垃圾回收的优点。我提问的主要原因是我知道Bjarne Stroustrup曾经说过C++在某个时间点会有一个垃圾回收器。
话虽如此,为什么还没有添加呢?已经有一些用于C++的垃圾回收器了。这只是那种“说起来容易做起来难”的事情吗?或者还有其他原因(并且在C++11中不会被添加)?
跨链接:
仅仅为了澄清一下,我理解C++在创立时没有垃圾回收器的原因。我想知道的是为什么不能添加回收器。
隐式垃圾回收可能已经被加入,但它没有被采纳。这可能不仅是由于实现上的复杂性,还因为人们无法快速达成共识。
以下是Bjarne Stroustrup的一句话:
我曾希望可选启用的垃圾回收器 将成为C++0x的一部分,但由于存在足够多的技术问题, 我只能提供关于此类回收器如何与语言的其余部分集成的详细说明。 与几乎所有C++0x功能一样,都存在实验性实现。
这里有一个很好的讨论(链接)。
总体概述:
C++非常强大,可以做几乎任何事情。出于这个原因,它不会自动将对性能产生影响的许多东西强制推给你。使用智能指针(将指针包装到具有引用计数的对象中,在引用计数达到0时自动删除自己)可以轻松实现垃圾回收。
C++考虑了那些没有垃圾回收的竞争对手。效率是C++必须抵御与C和其他语言相比的批评的主要关注点。
有两种类型的垃圾回收...
显式垃圾回收:
C++0x通过使用shared_ptr指针实现垃圾回收
如果你想要它,可以使用它;如果你不想用,也不会被强制使用。
在C++0x之前的版本中,存在boost:shared_ptr并具有相同的作用。
隐式垃圾回收:
尽管没有透明的垃圾回收,但它将成为未来C++语言规范的重点。
为什么Tr1没有隐式垃圾回收?
在之前的采访中,Bjarne Stroustrup曾表示,C++0x的tr1应该拥有更多的东西,但实际上并没有。
std::shared_ptr
)存在循环引用,会导致内存泄漏。因此,您必须仔细使用std::weak_ptr
来打破循环引用,但这很麻烦。标记清除式GC没有这个问题。线程和进程间并不与垃圾回收不兼容。Java和C#都具有高性能的抢占式多线程和垃圾收集器。对于实时应用程序和垃圾回收器,存在一些问题,因为大多数垃圾回收器必须停止运行才能进行垃圾回收。 - Andrew Tomazosstd::shared_ptr
)的主要问题是循环引用和糟糕的性能,这很讽刺,因为更好的性能通常是使用 C++ 的理由之一。请参见:http://flyingfrogblog.blogspot.co.uk/2011/01/boosts-sharedptr-up-to-10-slower-than.html - J D在这里补充一下讨论。
垃圾回收存在已知的问题,理解这些问题有助于理解为什么C ++中没有垃圾回收。
1. 性能?
第一个投诉通常是关于性能的,但大多数人并没有真正意识到自己在谈论什么。正如Martin Beckett所举例的那样,问题可能不在于性能本身,而在于性能的可预测性。
目前有两个广泛部署的GC家族:
Mark And Sweep更快(对整体性能影响较小),但它遭受“冻结世界”综合症的困扰:即当GC开始工作时,一切都会停止,直到GC完成清理。如果您希望构建能够在几毫秒内回答的服务器......某些事务将无法达到您的期望:)
引用计数的问题不同:引用计数增加了开销,特别是在多线程环境中,因为您需要具有原子计数。此外还存在引用循环的问题,因此需要聪明的算法来检测和消除这些循环(通常也会实现“冻结世界”,尽管不太频繁)。 一般来说,即使是更具响应性或者冻结不那么频繁的这种方式,它们今天也比Mark And Sweep慢。
我曾经看过一篇由Eiffel实现者撰写的论文,他们试图实现一个引用计数垃圾收集器,该垃圾收集器具有类似于Mark And Sweep的全局性能,但没有“Freeze The World”的方面。它需要一个单独的线程为GC服务(典型情况)。算法有点令人恐惧(到最后),但是论文很好地逐步介绍了各个概念,并展示了算法从“简单”版本到完整版本的演变。如果我能再次找到PDF文件,建议阅读......
2. 资源获取即初始化(RAII)
C++中有一个常见的习惯用法,就是将资源所有权封装在一个对象内部以确保其被适当释放。这主要用于内存管理,因为C++没有垃圾回收机制,但对于许多其他情况仍然非常有用:
其思想是正确控制对象的生命周期:
垃圾回收的问题在于,即使它有助于前者并最终保证后者……这种“最终”可能还不足够。如果您释放了锁,则真的希望现在就释放它,以便它不会阻止任何进一步的调用!
GC的语言有两个解决方法:
using
构造…但它是显式(弱)RAII,而在C++中,RAII是隐式的,以便用户不会无意中犯错误(省略using
关键字)。3. 智能指针
在C++中,智能指针经常被视为处理内存的银弹。我经常听到这样的说法:毕竟我们有了智能指针,所以我们不需要GC。
但没有比这更错的想法了。
智能指针确实有所帮助:auto_ptr
和unique_ptr
使用RAII概念,确实非常有用。它们非常简单,您可以很容易地自己编写它们。
然而,当需要共享所有权时,问题就变得更加困难:可能需要在多个线程间共享,而处理计数的一些微妙问题。因此,人们自然会使用shared_ptr
。
很好,这正是Boost的目的,但这并不是万能药。实际上,shared_ptr
的主要问题在于它模拟了一个通过引用计数实现的 GC,但你需要自己实现循环检测...累人。
当然,有这种东西叫做 weak_ptr
,但我不幸地发现,尽管使用了shared_ptr
,仍会因为这些循环而导致内存泄漏...而且当你处于多线程环境中时,很难检测到!
4. 解决方案?
没有银弹,但通常情况下是可行的。在没有垃圾回收机制的情况下,我们需要明确所有权:
weak_ptr
来打破它们所以,确实希望有一个 GC... 但这不是简单的问题。与此同时,我们只需挽起袖子。
应该优化哪种类型的嵌入式洗衣机控制器、手机、工作站还是超级计算机?
应该优先考虑 GUI 的响应速度还是服务器加载?
它应该使用大量内存还是大量 CPU?
C/C++ 在许多不同的情况下被使用。 我猜像 Boost 智能指针这样的东西对大多数用户来说已经足够了。
编辑-自动垃圾回收器并非性能问题(您总是可以购买更多服务器),而是可预测性能问题。
不知道垃圾回收什么时候启动就像雇佣一个患有睡眠病的航空飞行员,大多数情况下他们都很棒,但当你真正需要响应性能时!
C++没有内置垃圾回收机制的最大原因之一是:让垃圾回收与析构函数协同工作非常困难,据我所知,目前还没有完全解决这个问题的办法。以下是需要处理的许多问题:
这只是面临的一些问题。
free
(这里的free
类比于C语言)。但是Java从不保证调用finalizers或类似的内容。事实上,相对于Java,C++在运行时需要更多地处理数据库写操作、刷新文件句柄等。虽然Java声称拥有“GC”,但Java开发人员必须一丝不苟地经常调用close()
,并且他们必须非常注意资源管理,小心不要过早或过晚地调用close()
。而C++让我们摆脱了这种束缚。...(续) - Aaron McDaidtry(Whatever w = ...){...}
解决了它(如果您忘记了,还会收到警告)。剩下的一些问题也存在RAII中。每次调用“close()”意味着每万行可能只有一次,所以并不那么糟糕,而几乎在每个Java行上都会分配内存。 - maaartinusBjarne Stroustrup在这个问题上的回答如下:
我不喜欢垃圾。我不喜欢乱扔东西。我的理想是通过不产生任何垃圾来消除需要垃圾收集器的需求。现在已经可能了。
对于现代C++(C++17及其以上版本,并遵循官方核心指南)编写的代码,情况如下:
确实,您可以无视所有准则编写有泄漏的应用程序代码 - 它将编译并运行(并泄漏),与以往相同。
但这不是一个“只是不要那样做”的情况,在这种情况下,开发人员被期望具有美德并自我控制;编写不符合规范的代码既不简单,也不更快,也不具有更好的性能。随着时间的推移,编写此类代码也将变得更加困难,因为您将面临与符合规范的代码提供和期望的“阻抗不匹配”。
确实,如果你下定决心,可以编写破坏规则的代码。但:
如果您是C++库开发人员,那么您需要编写涉及原始指针的不安全代码,并且需要小心、负责任地编码 - 但这些都是由专家编写(更重要的是,由专家审查)的自包含代码。
所以,就像Bjarne所说:通常没有动力来收集垃圾,因为您几乎不会产生垃圾。对于C ++而言,GC已经成为一个非问题。
这并不是说GC不是某些特定应用程序的有趣问题,当您想要使用自定义分配和解除分配策略时,您需要的是自定义分配和解除分配,而不是语言级别的GC。
来源:http://www.stroustrup.com/bs_faq.html#garbage-collection
至于为什么它没有内置垃圾回收,如果我没记错的话,它是在GC成为“事物”之前发明的,而且我认为该语言由于多种原因(例如与C的向后兼容性)可能无法拥有GC。
希望这可以帮到您。
Stroustrup在2013年的Going Native会议上对此发表了一些很好的评论。
可以直接跳到 这个视频 的25分50秒左右。(实际上我建议观看整个视频,但这段可以直接跳到有关垃圾回收的内容。)
当你拥有一种非常好的语言,它可以直接处理对象和值,避免(显式)使用堆栈,并且易于使用(安全、可预测、易读、易教),那么你甚至不需要垃圾回收。
使用现代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持有打开的文件句柄、连接到数据库或缓冲写入数据库的数据,则此方法不够好,因为我们希望尽快销毁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”。该对象在函数结束时被销毁。如果你想要这个效果,那么这非常直观。“Gadget”像“int”或“double”一样表现。易于预测,易于阅读和教学。一切都是“值”。有时候是大值,但是值比指针(或引用)更容易教授,因为你没有这种“远程操作”的事情。Gadget
不再要求其他实体代表其执行操作,那么在Java中,如果删除无意义(对Java而言)的delete
语句,则原始代码将是完全安全的。 - supercatshared_ptr<T>
。它可以决定不为该类型实际管理引用计数器,而是使用GC。这将允许在开发人员不需要注意的情况下使用GC。对于适当的T,shared_ptr
可以简单地被视为GC指针。但是这种方法存在局限性,并且会使许多程序变慢。 - Aaron McDaidString
是不是不可变的?你之前谈到了读和写,但对于一个不可变的 String
来说,写入是没有意义的。可以考虑使用类似 using String = const std::vector<char>;
的方式。在明确类型之前,我无法考虑其他问题。 - Aaron McDaidC++ 的设计理念是:如果你不使用某些功能,就不必为其支付性能上的代价。因此,添加垃圾回收机制意味着有些程序可以像 C 语言一样直接在硬件上运行,而有些则需要在运行时虚拟机中运行。
并没有什么阻止你使用某种绑定到第三方垃圾回收机制的智能指针。我记得微软曾经在 COM 中做过类似的事情,但效果不太好。