首先,因为垃圾收集的优点,所以我没有问这个问题。我提出这个问题的主要原因是,我确实知道Bjarne Stroustrup曾说过C ++将在某个时间点具有垃圾收集器。
话虽如此,为什么不添加呢?已经有一些C ++的垃圾收集器。这仅仅是那些“说起来容易做起来难”的事情之一吗?还是有其他原因未添加(并且不会在C ++ 11中添加)?
交叉链接:
为了澄清起见,我理解C ++首次创建时没有垃圾收集器的原因。我想知道为什么不能添加收集器。
首先,因为垃圾收集的优点,所以我没有问这个问题。我提出这个问题的主要原因是,我确实知道Bjarne Stroustrup曾说过C ++将在某个时间点具有垃圾收集器。
话虽如此,为什么不添加呢?已经有一些C ++的垃圾收集器。这仅仅是那些“说起来容易做起来难”的事情之一吗?还是有其他原因未添加(并且不会在C ++ 11中添加)?
交叉链接:
为了澄清起见,我理解C ++首次创建时没有垃圾收集器的原因。我想知道为什么不能添加收集器。
Answers:
可以添加隐式垃圾回收,但是并没有成功。可能不仅是由于实施方面的复杂性,还因为人们没有足够快地达成普遍共识。
Bjarne Stroustrup自己的话:
我曾希望可以选择启用的垃圾收集器将成为C ++ 0x的一部分,但是我有足够的技术问题需要解决,仅需详细说明如何将垃圾收集器与其他语言集成(如果提供)。与基本上所有C ++ 0x功能的情况一样,存在实验性实现。
有话题商量好了这里。
总体概述:
C ++功能非常强大,几乎可以做任何事情。因此,它不会自动将很多可能影响性能的事情推到您身上。垃圾回收可以通过智能指针轻松实现(智能对象包装带有参考计数的指针,当参考计数达到0时,这些对象会自动删除自身)。
C ++是在考虑没有垃圾收集的竞争对手的情况下构建的。与C和其他语言相比,效率是C ++必须抵制批评的主要问题。
垃圾收集有2种类型...
显式垃圾回收:
C ++ 0x将通过使用shared_ptr创建的指针进行垃圾回收
如果您想要它,则可以使用它;如果您不想要它,则不会被迫使用它。
如果您不想等待C ++ 0x,则当前还可以使用boost:shared_ptr。
隐式垃圾回收:
它没有透明的垃圾收集器。不过,它将成为未来C ++规范的重点。
为什么Tr1没有隐式垃圾回收?
C ++ 0x的tr1应该有很多东西,Bjarne Stroustrup在之前的采访中指出,tr1并不如他所愿。
smart_ptr's
?您将如何使用垃圾收集器进行低级Unix样式的分叉?其他因素也会受到影响,例如穿线。Python之所以具有全局解释器锁,主要是因为它具有垃圾回收功能(请参见Cython)。请将其放在C / C ++中,谢谢。
std::shared_ptr
)的主要问题是循环引用,这会导致内存泄漏。因此,您必须谨慎使用std::weak_ptr
以打破周期,这很麻烦。标记和扫描样式GC不会出现此问题。线程/派生与垃圾回收之间没有固有的不兼容性。Java和C#都具有高性能的抢占式多线程和垃圾回收器。实时应用程序和垃圾收集器存在一些问题,因为大多数垃圾收集器必须停止运行。
std::shared_ptr
)是循环引用”和可怕的表现是具有讽刺意味,因为更好的性能,通常是用C ++ ...的理由flyingfrogblog.blogspot.co.uk/2011/01/...
在这里增加辩论。
垃圾回收存在一些已知问题,对它们的了解有助于理解C ++中为什么没有垃圾回收的问题。
1.性能?
最初的抱怨通常是关于性能的,但是大多数人并没有真正意识到他们在说什么。如图所示,Martin Beckett
问题可能不是性能本身,而是性能的可预测性。
当前有2个广泛使用的GC系列:
该Mark And Sweep
快(对整体性能的影响较小),但它从一个“冻结世界”综合症患有:即当在GC踢,一切停止,直到GC作出了清理。如果您希望构建可以在几毫秒内响应的服务器...某些事务将无法达到您的期望:)
问题Reference Counting
不同:引用计数会增加开销,尤其是在多线程环境中,因为您需要具有原子计数。此外,还有参考循环的问题,因此您需要一个聪明的算法来检测这些循环并消除它们(通常也通过“冻结世界”来实现,尽管频率较低)。通常,截至今天,这种方法(尽管通常响应速度更快,或者冻结的频率降低了)要比慢Mark And Sweep
。
我看过埃菲尔铁塔实施者的论文,他们试图实施一个Reference Counting
垃圾收集器,该垃圾收集器的全球性能与Mark And Sweep
没有“冻结世界”方面的相似。它需要用于GC的单独线程(典型)。该算法有点令人恐惧(最后),但是论文很好地完成了一次介绍一个概念的过程,并展示了该算法从“简单”版本到成熟版本的演变。推荐阅读,只要我能把双手放回PDF文件上...
2.资源获取正在初始化(RAII)
这是一个常见的习惯用法C++
,您将资源的所有权包装在一个对象中以确保正确释放它们。因为我们没有垃圾回收,所以它主要用于内存,但是它在许多其他情况下也很有用:
这个想法是适当地控制对象的生存期:
GC的问题是,如果它对前者有所帮助并最终保证在以后……“最终”可能还不够。如果您释放一个锁,那么您真的希望它现在被释放,这样它就不会阻止任何进一步的呼叫!
使用GC的语言有两种解决方法:
using
构造...但是它是显式的(弱)RAII,而在C ++中,RAII是隐式的,因此用户不能不经意间就犯错误(通过省略using
关键字)3.智能指针
智能指针通常作为处理内存中的银弹出现C++
。我常常听到:由于我们有智能指针,所以我们毕竟不需要GC。
一个再错不过了。
智能指针确实有帮助:auto_ptr
并unique_ptr
使用RAII概念,的确非常有用。它们是如此简单,您可以很容易地自己编写它们。
但是,当需要共享所有权时,它将变得更加困难:您可能会在多个线程之间共享,并且计数处理存在一些细微的问题。因此,自然会走向shared_ptr
。
太好了,毕竟这就是Boost的目的,但这不是万灵丹。实际上,主要的问题shared_ptr
是它模拟了由GC实施的GC,Reference Counting
但是您需要自己全部实施循环检测... Urg
当然,这很weak_ptr
麻烦,但是尽管如此,shared_ptr
由于使用了这些周期,但不幸的是我已经看到内存泄漏了……而且当您处于多线程环境中时,这很难检测!
4.解决方案是什么?
没有万灵药,但与往常一样,这绝对是可行的。在没有GC的情况下,需要明确所有权:
weak_ptr
所以确实,拥有GC很棒。。。但这不是小问题。同时,我们只需要卷起袖子。
什么样的?是否应针对嵌入式洗衣机控制器,手机,工作站或超级计算机进行优化?
它应该优先考虑GUI响应性或服务器加载吗?
应该使用大量内存还是大量CPU?
C / c ++用于太多不同的情况。我怀疑诸如boost智能指针之类的东西对于大多数用户来说已经足够了
编辑-自动垃圾收集器并不是性能的问题(您可以随时购买更多服务器),而只是性能的问题。
不知道GC何时启动,就像聘用一名麻醉师的飞行员一样,在大多数情况下,他们都很棒-但当您真正需要响应时!
C ++没有内置垃圾回收的最大原因之一是,让垃圾回收与析构函数很好地配合确实非常困难。据我所知,还没有人真正知道如何彻底解决它。有很多问题要处理:
这些只是面临的一些问题。
Dispose
对象可能会使它变得不稳定,但是在对象存活时指向该对象的引用将在它死后继续这样做。相比之下,在非GC系统中,可以在存在引用的情况下删除对象,并且如果使用这些引用之一,则对破坏的危害几乎没有任何限制。
尽管这是一个古老的问题,但仍然有一个我完全看不到的问题:几乎不可能指定垃圾回收。
特别是,C ++标准非常谨慎地从外部可观察到的行为而不是实现如何实现该行为的角度指定语言。在垃圾回收的情况下,然而,是几乎没有外部观察到的行为。
垃圾回收的一般想法是,它应该做出合理的尝试来确保内存分配成功。不幸的是,即使您确实有垃圾收集器在运行,也几乎不可能保证任何内存分配都会成功。无论如何在某种程度上都是如此,但是对于C ++来说尤其如此,因为(可能)不可能使用复制收集器(或类似的东西)在收集周期内将对象移动到内存中。
如果您不能移动对象,则无法创建一个连续的内存空间来进行分配-这意味着您的堆(或免费存储,或您喜欢的任何名称)可以并且可能会,随着时间的流逝变得支离破碎。反过来,这可以防止分配成功,即使有更多的可用内存超出请求的数量。
尽管可能会提出一些保证,(实质上)说,如果您重复重复完全相同的分配模式,并且第一次成功,那么只要分配的内存,它将在后续迭代中继续成功在迭代之间变得不可访问。这是一个微弱的保证,实际上是没有用的,但是我看不出有任何合理的希望来加强它。
即使这样,它也比C ++所提出的要强大。在先前的提案 [警告:PDF](即得到下降)根本不保证任何事情。在28页的提案中,您从外部可观察到的行为得到的是一条(非规范性的)注释,说:
[注意:对于垃圾回收程序,高质量的托管实现应尝试最大程度地回收其无法访问的内存。—尾注]
至少对我而言,这引起了有关投资回报率的严重问题。我们将破坏现有的代码(没人知道确切多少,但肯定很多),对实现提出新的要求,并对代码施加新的限制,而我们得到的回报很可能根本不算什么?
即使充其量,我们所获得的程序都是基于Java进行的测试,以与现在相同的速度运行可能需要大约六倍的内存。更糟糕的是,垃圾收集从一开始就是Java的一部分-C ++对垃圾收集器施加了更多限制,使其几乎肯定会具有更差的成本/收益比(即使我们超出了提案所保证的范围并假定会有一些好处)。
我用数学的方式总结了这种情况:这是一个复杂的情况。正如任何数学家所知,复数有两个部分:实数和虚数。在我看来,这里的成本是真实的,但收益(至少大部分是虚构的)。
free
您(我的意思free
是C语言的意思)。但是Java不能保证调用终结器或类似的东西。实际上,C ++在执行提交数据库写入,刷新文件句柄等操作方面比Java发挥更大的作用。Java声称具有“ GC”,但是Java开发人员必须close()
时刻认真地进行调用,并且他们必须非常了解资源管理,请注意不要调用close()
得太早或太晚。C ++使我们摆脱了困境。...(续)
try (Whatever w=...) {...}
可以解决(忘记时会收到警告)。其余的也与RAII有关。close()
始终“ 调用”意味着可能每万行执行一次,所以还算不错,而几乎在每条Java行上都分配了内存。
如果您想要自动垃圾收集,那么有一些不错的C ++商业和公共领域垃圾收集器。对于适合垃圾回收的应用程序,C ++是一种出色的垃圾回收语言,其性能可与其他垃圾回收语言相比。有关C ++中自动垃圾收集的讨论,请参见C ++编程语言(第4版)。另请参见Hans-J。Boehm的C和C ++垃圾收集站点(存档)。
而且,C ++支持编程技术,这些技术允许内存管理在没有垃圾收集器的情况下安全且隐式地进行。我认为垃圾回收是资源管理的最后选择和不完善的处理方式。这并不意味着它永远不会有用,只是在许多情况下有更好的方法。
资料来源:http : //www.stroustrup.com/bs_faq.html#garbage-collection
至于为什么它没有内置它,如果我没记错的话,它是在GC才是东西之前发明的,而且我不认为该语言可能有GC的原因有很多(IE Backwards与C兼容)
希望这可以帮助。
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拥有一个打开的文件句柄,与数据库的连接,或者您缓冲以供稍后写入数据库的数据,这还不够。我们希望小工具在完成后立即销毁,以便尽快释放这些资源。您不希望您的数据库服务器在数以千计的不再需要的数据库连接中苦苦挣扎-它不知道您的程序已完成工作。
那么解决方案是什么?有几种方法。用于绝大多数对象的显而易见的方法是:
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
。可预测,易于阅读,易于教学。一切都是“价值”。有时值很大,但是值更容易讲授,因为您没有指针(或引用)获得的“远距离操作”。
您制作的大多数对象仅在创建它们的函数中使用,并且可能作为输入传递给子函数。程序员在返回对象或在软件的各个不同部分之间共享对象时,不必考虑“内存管理”。
范围和寿命很重要。在大多数情况下,寿命与示波器相同会更容易。它更容易理解和教学。当您想要不同的生命周期时,shared_ptr
例如使用来读取正在执行此操作的代码应该很明显。(或利用移动语义或,按值返回(大)对象unique_ptr
。
这似乎是效率问题。如果我要从中返回小工具foo()
怎么办?C ++ 11的移动语义使返回大对象变得更加容易。只需编写Gadget foo() { ... }
,它就可以正常工作,并且工作很快。您无需弄乱&&
自己,只需按值返回事物,该语言通常就可以进行必要的优化。(即使在C ++ 03之前,编译器在避免不必要的复制方面也做得非常好。)
正如斯特鲁斯特鲁普(Stroustrup)在视频中其他地方所说的(措辞):“只有计算机科学家会坚持复制对象,然后销毁原始对象。(听众大笑)。为什么不将对象直接移动到新位置呢?这就是人类(不是计算机科学家)期望的。”
当您可以保证只需要一个对象的一个副本时,了解对象的生存期就容易得多。您可以选择所需的生命周期策略,并且可以根据需要进行垃圾收集。但是,当您了解其他方法的好处时,您会发现垃圾回收位于首选项列表的底部。
如果那对您不起作用,则可以使用unique_ptr
,或使用失败的shared_ptr
。在内存管理方面,写得很好的C ++ 11比许多其他语言更短,更易读,更易教。
Gadget
不要求其他东西代表它做任何事情,那么如果delete
删除了对Java毫无意义的语句,则原始代码在Java中将是绝对安全的。
shared_ptr<T>
在T
“无聊” 时特别对待。它可以决定不实际管理该类型的引用计数器,而是使用GC。这将允许使用GC,而无需开发人员注意。对于A,shared_ptr
可以简单地将A 视为GC指针T
。但是这样做有局限性,它会使许多程序变慢。
string1=string2;
将执行非常迅速,无论字符串的长度(它的字面意思无非寄存器加载和登记店铺),并且不需要任何锁定,以确保如果上述声明的同时执行string2
IS被写入时,string1
将保留旧值或新值,并且没有未定义行为)。
shared_ptr<String>
需要大量的后台同步,String
如果同时读取和写入变量,则a的分配可能会表现得很奇怪。人们想同时写和读的String
情况并不是很常见,但如果例如某些代码希望使正在进行的状态报告可用于其他线程,则可能发生。在.NET和Java中,这样的事情“有效”。
Bjarne Stroustrup 对此问题的常见问题解答说:
我不喜欢垃圾 我不喜欢乱扔垃圾。我的理想是通过不产生任何垃圾来消除对垃圾收集器的需求。现在这是可能的。
对于当前编写的代码(C ++ 17和遵循官方 Core Guidelines)的情况如下:
确实,您可以不理会所有准则,而编写泄漏的应用程序代码-它会像往常一样编译和运行(和泄漏)。
但这不是“只是不做那件事”的情况,在这种情况下,期望开发人员是有道德的,并能进行很多自我控制。编写不合格的代码并不简单,编写起来也不快,性能也不更好。逐渐地,编写它也将变得更加困难,因为您将面临越来越多的“阻抗失配”,这与符合规范的代码所提供和期望的内容不符。
reintrepret_cast
呢?还是做复杂的指针算术?还是其他此类黑客?”确实,如果您下定决心,尽管编写指南得当,但您仍可以编写使事情变得混乱的代码。但:
如果您是C ++库开发人员,那么您确实会编写涉及原始指针的不安全代码,并且需要认真负责地进行编码-但这是专家编写的独立代码(更重要的是,经过专家审查)。
因此,就像Bjarne所说的那样:基本上没有收集垃圾的动机,因为大家都确保不要产生垃圾。使用C ++,GC不再是问题。
这并不是说当您要使用自定义分配和取消分配策略时,对于某些特定应用程序,GC并不是一个有趣的问题。对于那些您想要自定义分配和取消分配,而不是语言级别的GC。
C ++背后的想法是,您不会为不使用的功能付出任何性能影响。因此,添加垃圾回收将意味着让某些程序像C一样直接在硬件上运行,而某些程序则在某种运行时虚拟机中运行。
没有什么可以阻止您使用绑定到某些第三方垃圾收集机制的某种形式的智能指针。我似乎记得微软用COM做类似的事情,但进展并不顺利。
要回答有关C ++的大多数“为什么”问题,请阅读C ++的设计和演变。
原始C语言背后的基本原理之一是内存由一系列字节组成,并且代码仅需关心这些字节在使用时的确切含义。现代C允许编译器施加其他限制,但是C包括(并且C ++保留了)以下功能:将指针分解为字节序列,将包含相同值的任何字节序列组合为指针,然后使用该指针访问先前的对象。
尽管该功能在某些应用程序中可能是有用的,甚至是必不可少的,但是包含该功能的语言在支持任何有用且可靠的垃圾收集方面的能力将非常有限。如果编译器不了解组成指针的位所完成的所有操作,则它将无法知道宇宙中是否存在足以重建指针的信息。因为有可能以一种即使计算机知道信息也无法访问的方式来存储信息(例如,组成指针的字节可能已经在屏幕上显示了足够长的时间,以便有人编写)将它们放到一张纸上),计算机实际上不可能知道将来是否可以使用指针。
许多垃圾收集框架的一个有趣的怪癖是,对象引用不是由其中包含的位模式定义的,而是由对象引用中保留的位与其他地方保存的其他信息之间的关系定义的。在C和C ++中,如果存储在指针中的位模式标识一个对象,则该位模式将标识该对象,直到该对象被明确销毁为止。在典型的GC系统中,对象可能在某个时刻由位模式0x1234ABCD表示,但是下一个GC周期可能会将对0x1234ABCD的所有引用替换为对0x4321BABE的引用,因此该对象将由后一种模式表示。即使要显示与对象引用关联的位模式,然后再从键盘读回它,
所有的技术讨论都使这个概念变得过于复杂。
如果自动将GC放入所有内存的C ++中,请考虑使用Web浏览器之类的方法。Web浏览器必须加载完整的Web文档并运行Web脚本。您可以将Web脚本变量存储在文档树中。在浏览器中打开了许多标签的BIG文档中,这意味着每次GC必须进行完整收集时,它还必须扫描所有文档元素。
在大多数计算机上,这意味着将发生PAGE FAULTS。因此,回答该问题的主要原因是将发生PAGE FAULTS。您将在PC开始进行大量磁盘访问时知道这一点。这是因为GC必须触摸大量内存才能证明无效的指针。当您拥有一个真正的使用大量内存的应用程序时,由于页面故障,必须扫描每个集合的所有对象,这很麻烦。页面错误是当虚拟内存需要从磁盘读回RAM时。
因此,正确的解决方案是将应用程序分为需要GC的部分和不需要GC的部分。在上面的Web浏览器示例中,如果文档树是使用malloc分配的,而javascript是使用GC来运行的,则每次GC踢进来时,它只会扫描一小部分内存,并扫描内存的所有PAGED OUT元素以查找文档树不需要重新调回。
为了进一步了解此问题,请查看虚拟内存及其在计算机中的实现方式。这完全是因为当内存不足时,程序可以使用2GB。在带有2GB RAM且用于32BIt系统的现代计算机上,只要仅运行一个程序,就不会出现此问题。
作为另一个示例,考虑一个必须跟踪所有对象的完整集合。首先,您必须扫描通过根可访问的所有对象。第二次扫描步骤1中所有可见的对象。然后扫描等待的析构函数。然后再次转到所有页面并关闭所有不可见的对象。这意味着许多页面可能会被换出并多次返回。
因此,我想简单地回答一下:由于触摸所有内存而导致的PAGE FAULTS数量导致程序中所有对象的完整GC不可行,因此程序员必须将GC视为脚本之类的辅助工具和数据库工作,但是通过手动内存管理可以完成正常工作。
当然,另一个非常重要的原因是全局变量。为了使收集器知道全局变量指针位于GC中,它将需要特定的关键字,因此现有的C ++代码将不起作用。
简短的回答:我们不知道如何有效地进行垃圾收集(时间和空间开销较小),并且始终正确(在所有可能的情况下)。
长答案:就像C一样,C ++是一种系统语言。这意味着在编写系统代码(例如操作系统)时使用它。换句话说,与C一样,C ++被设计为具有最佳性能为主要目标。该语言的标准不会添加任何可能妨碍性能目标的功能。
这暂停了一个问题:为什么垃圾回收会阻碍性能?主要原因是,在实现方面,我们(计算机科学家)不知道在所有情况下如何以最小的开销进行垃圾回收。因此,C ++编译器和运行时系统不可能一直有效地执行垃圾回收。另一方面,C ++程序员应该知道他的设计/实现,并且他是决定如何最好地进行垃圾收集的最佳人选。
最后,如果控制(硬件,详细信息等)和性能(时间,空间,功率等)不是主要约束,则C ++不是编写工具。其他语言可能会更好,并且会提供更多的[隐藏]运行时管理,并具有必要的开销。
当我们将C ++与Java进行比较时,我们发现C ++在设计时并未考虑隐式垃圾回收,而Java是。
在C-Style中使用任意指针之类的东西不仅不利于GC实现,而且还会破坏大量C ++-legacy-code的向后兼容性。
除此之外,C ++是一种旨在作为独立的可执行文件运行的语言,而不是具有复杂的运行时环境。
总而言之:是的,可以将垃圾收集添加到C ++,但是为了连续性,最好不要这样做。
主要有两个原因:
C ++已经提供了手动内存管理,堆栈分配,RAII,容器,自动指针,智能指针...就足够了。垃圾收集器适用于懒惰的程序员,他们不想花5分钟的时间思考谁应该拥有哪些对象或何时应该释放资源。那不是我们在C ++中做事的方式。
强加垃圾回收实际上是从低级到高级的范式转换。
如果您看一看用垃圾回收语言处理字符串的方式,您会发现它们仅允许高级字符串操作功能,并且不允许二进制访问字符串。简而言之,所有字符串函数都将首先检查指针以查看字符串在哪里,即使您仅绘制一个字节也是如此。因此,如果您要执行一个循环,以带有垃圾回收的语言处理字符串中的每个字节,则该循环必须计算每次迭代的基本位置和偏移量,因为它无法知道字符串何时移动。然后,您必须考虑堆,堆栈,线程等。