我不是问这个问题,因为垃圾收集的优点首先.我提出这个问题的主要原因是我知道Bjarne Stroustrup已经说过C++在某些时候会有一个垃圾收集器.
话虽如此,为什么还没有添加?已经有一些垃圾收集器用于C++.这只是那些"说起来容易做起来难"的事情吗?还是有其他原因没有添加(并且不会在C++ 11中添加)?
交叉链接:
用于C++的垃圾收集器
为了澄清,我理解为什么C++在第一次创建时没有垃圾收集器的原因.我想知道为什么收藏家不能加入.
可能已经添加了隐式垃圾收集,但它只是没有削减.可能由于不仅仅是实施并发症,而且还因为人们无法以足够快的速度达成普遍共识.
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++中没有垃圾收集的原因.
1.表现?
第一个抱怨往往是关于表现,但大多数人并没有真正意识到他们在谈论什么.如图所示,Martin Beckett
问题可能不是性能本身,而是性能的可预测性.
目前有2个GC系列广泛部署:
Mark-And-Sweep kind
参考 - 计数种类
该Mark And Sweep
快(对整体性能的影响较小),但它从一个"冻结世界"遭受综合征:即当在,其他一切都停止在GC踢,直到GC作出了清理.如果你想构建一个在几毫秒内回答的服务器...某些交易将达不到你的期望:)
问题Reference Counting
是不同的:引用计数会增加开销,尤其是在多线程环境中,因为您需要具有原子计数.此外还存在参考周期的问题,因此您需要一个聪明的算法来检测这些周期并消除它们(通常通过"冻结世界"来实现,尽管不那么频繁).一般来说,截至今天,这种类型(即使通常更敏感或更确切地说,冷冻不那么频繁)比慢Mark And Sweep
.
我看过埃菲尔实施者的一篇论文,他们试图实现一个Reference Counting
垃圾收集器,它具有类似的全球性能而Mark And Sweep
没有"冻结世界"的方面.它需要一个单独的GC线程(典型值).这个算法有点令人恐惧(最后),但是本文很好地一次介绍了一个概念,并展示了算法从"简单"版本到完整版本的演变.推荐阅读,只要我能把手放回PDF文件......
2.资源获取是初始化
这是一个常见的习惯用法C++
,你将把资源的所有权包装在一个对象中,以确保它们被正确释放.它主要用于内存,因为我们没有垃圾收集,但它在许多其他情况下也很有用:
锁(多线程,文件句柄,......)
连接(到数据库,另一台服务器......)
我们的想法是正确控制对象的生命周期:
只要你需要它就应该活着
当你完成它应该被杀死
GC的问题在于,如果它有助于前者并最终保证以后......这种"终极"可能还不够.如果你发布一个锁,你真的希望它现在被释放,这样它就不会阻止任何进一步的调用!
GC的语言有两种解决方法:
堆栈分配足够时不要使用GC:它通常用于性能问题,但在我们的情况下它确实有帮助,因为范围定义了生命周期
using
构造...但它是显式(弱)RAII而在C++中RAII是隐式的,因此用户不会无意中犯错(通过省略using
关键字)
3.智能指针
智能指针通常表现为处理内存的银弹C++
.我经常听说过:毕竟我们不需要GC,因为我们有智能指针.
一个人不能错.
智能指针确实有帮助:auto_ptr
并unique_ptr
使用RAII概念,确实非常有用.它们非常简单,你可以很容易地自己编写它们.
当需要共享所有权时,它会变得更加困难:您可能在多个线程之间共享,并且计数的处理存在一些微妙的问题.因此,人们自然会走向shared_ptr
.
这很棒,毕竟这就是Boost,但它不是一颗银弹.事实上,主要的问题shared_ptr
是它模拟了一个GC实现,Reference Counting
但你需要自己实现循环检测... Urg
当然有这个weak_ptr
东西,但遗憾的是我已经看到了内存泄漏,尽管shared_ptr
因为这些循环而使用......当你处于多线程环境时,它很难被发现!
4.解决方案是什么?
没有银弹,但一如既往,它绝对可行.在没有GC的情况下,需要明确所有权:
如果可能的话,我希望在一个给定时间拥有一个所有者
如果没有,请确保您的类图没有任何与所有权相关的循环,并以微妙的应用程序打破它们 weak_ptr
事实上,拥有一个GC会很棒......但这不是一个小问题.与此同时,我们只需要卷起袖子.
什么类型?它应该针对嵌入式洗衣机控制器,手机,工作站还是超级计算机进行优化?
它应该优先考虑gui响应能力还是服务器负载?
它应该使用大量内存还是大量CPU?
C/c ++用于太多不同的情况.我怀疑像升级智能指针这样的东西对大多数用户来说已经足够了
编辑 - 自动垃圾收集器不是一个性能问题(你总是可以买更多的服务器)这是一个可预测的性能问题.
不知道GC什么时候开始就像雇用一个嗜睡的航空公司飞行员,大部分时间他们都很棒 - 但是当你真的需要响应时!
C++没有内置垃圾收集的最大原因之一是让垃圾收集与析构函数一起使用真的很难.据我所知,没有人真正知道如何完全解决它.有很多问题需要处理:
对象的确定性生命周期(引用计数给你这个,但GC没有.虽然它可能不是那么大的交易).
如果在对象被垃圾收集时析构函数抛出会发生什么?大多数语言都忽略了这个异常,因为它确实没有阻止能够传输它,但这可能不是C++可接受的解决方案.
如何启用/禁用它?当然,它可能是一个编译时决定,但是为GC编写的代码和为非GC编写的代码将会非常不同并且可能不兼容.你如何调和这个?
这些只是面临的一些问题.
虽然这是一个老问题,但仍有一个问题我根本没有看到任何人解决过:垃圾收集几乎无法指定.
特别是,C++标准非常谨慎地根据外部可观察行为来指定语言,而不是实现如何实现该行为.在垃圾回收的情况下,然而,是几乎没有外部观察到的行为.
垃圾收集的一般思想是它应该合理地尝试确保内存分配成功.不幸的是,即使你有垃圾收集器在运行,基本上也不可能保证任何内存分配都会成功.在任何情况下都是如此,但在C++的情况下尤其如此,因为它(可能)不可能使用在收集周期期间在内存中移动对象的复制收集器(或类似的东西).
如果您无法移动对象,则无法创建一个连续的内存空间来进行分配 - 这意味着您的堆(或免费存储,或者您喜欢称之为的任何内容)可以,并且可能会随着时间的推移变得支离破碎.反过来,这可以防止分配成功,即使存在比所请求的数量更多的内存.
虽然有可能提出一些保证说(实质上)如果重复完全重复相同的分配模式,并且第一次成功,它将继续在后续迭代中成功,前提是分配的内存在迭代之间变得无法访问.这是一个如此薄弱的保证,它本质上是无用的,但我看不出任何加强它的合理希望.
即使这样,它也比C++提出的要强.在先前的提案 [警告:PDF](即得到下降)并不能保证在所有的东西.在28页的提案中,你对外部可观察行为的看法是单一(非规范性)说明:
[注意:对于垃圾收集程序,高质量的托管实现应该尝试最大化它回收的无法访问的内存量. - 尾注]
至少对我而言,这引发了一个关于投资回报的严重问题.我们将破坏现有的代码(没有人确切地确定了多少,但绝对相当多),对实现和代码的新限制提出了新的要求,而我们获得的回报很可能一点都没有?
即便充其量,我们得到的是基于Java测试的程序,可能需要大约六倍的内存才能以与现在相同的速度运行.更糟糕的是,垃圾收集从一开始就是Java的一部分--C++对垃圾收集器施加了更多的限制,它几乎肯定会有更差的成本/收益比(即使我们超出了提案所保证的范围并假设会有一些好处).
我将以数学方式总结这种情况:这是一个复杂的情况.正如任何数学家所知,复数有两部分:真实的和虚构的.在我看来,我们这里所拥有的是真实的成本,但是(至少大部分)想象的好处.
如果你想要自动垃圾收集,那么C++就有很好的商业和公共域垃圾收集器.对于适合垃圾收集的应用程序,C++是一种优秀的垃圾收集语言,其性能可与其他垃圾收集语言相媲美.有关C++中自动垃圾收集的讨论,请参阅C++编程语言(第3版).另见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持有一个打开的文件句柄,或者一个数据库连接,或者你为了稍后写入数据库而缓冲的数据,这就不够了.我们希望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
s表现与int
或相同double
.可预测,易于阅读,易于教学.一切都是'价值'.有时价值很高,但价值观更容易教,因为你没有用指针(或引用)得到的"远距离"动作.
您创建的大多数对象仅用于创建它们的函数,并且可能作为输入传递给子函数.程序员在返回对象时不必考虑"内存管理",或者在软件的广泛分离部分之间共享对象.
范围和寿命很重要.大多数情况下,如果寿命与范围相同则更容易.它更容易理解,更容易教学.当你想要一个不同的生命周期时,通过使用shared_ptr
例如,阅读你正在做的这些代码应该是显而易见的.(或者按值返回(大)对象,利用移动语义或unique_ptr
.
这似乎是一个效率问题.如果我想从中返回一个小工具foo()
怎么办?C++ 11的移动语义使得返回大对象变得更容易.只需写Gadget foo() { ... }
,它就会起作用,并且可以快速工作.你不需要弄乱&&
自己,只需按价值返回东西,语言通常就能做必要的优化.(甚至在C++ 03之前,编译器在避免不必要的复制方面做得非常好.)
正如Stroustrup在视频中的其他地方所说的那样:"只有计算机科学家会坚持复制一个物体,然后摧毁原件.(观众笑).为什么不直接将物体移动到新的位置?这就是人类(不是计算机科学家)期望的."
当您可以保证只需要一个对象的副本时,就可以更容易地理解对象的生命周期.您可以选择所需的生命周期策略,如果需要,可以使用垃圾收集.但是当您了解其他方法的好处时,您会发现垃圾收集位于首选项列表的底部.
如果这对您不起作用,您可以使用unique_ptr
或失败shared_ptr
.在内存管理方面,编写得很好的C++ 11比其他许多语言都更短,更容易阅读,也更容易教授.
C++背后的想法是,您不会为不使用的功能支付任何性能影响.因此,添加垃圾收集意味着让一些程序直接在硬件上以C的方式运行,某些程序在某种运行时虚拟机中运行.
没有什么能阻止您使用绑定到某些第三方垃圾收集机制的某种形式的智能指针.我似乎记得微软用COM做了类似的事情,并没有顺利进行.
要回答关于C++的大多数"为什么"的问题,请阅读C++的设计和演变
Bjarne Stroustrup 关于此事的常见问题答案说:
我不喜欢垃圾.我不喜欢乱扔垃圾.我的理想是通过不产生任何垃圾来消除对垃圾收集器的需求.现在这是可能的.
对于这些天编写的代码(C++ 17并遵循官方 核心指南),情况如下:
大多数与内存所有权相关的代码都在库中(特别是那些提供容器的代码)
大多数使用涉及内存所有权的代码遵循RAII 模式,因此在构造和销毁时释放时进行分配,这在退出分配内容的范围时发生.
您没有直接显式分配或释放内存.
原始指针不拥有内存(如果你遵循了指导原则),所以你不能通过传递它们来泄漏.
如果你想知道如何在内存中传递值序列的起始地址 - 你将用一个跨度来做; 没有原始指针需要.
如果你真的需要一个拥有"指针",你使用C++的 标准库智能指针 - 它们不会泄漏,而且非常有效.或者,您可以使用"所有者指针"跨范围边界传递所有权.这些是不常见的,必须明确使用; 它们允许对泄漏进行部分静态检查.
实际上,您可以忽略所有指导原则并编写泄漏的应用程序代码 - 它将一如既往地编译和运行(和泄漏).
但这不是"只是不要那样做"的情况,开发人员应该是善良的,并且要进行大量的自我控制; 编写不符合规范的代码并不简单,编写速度也不快,也不是表现更好.逐渐地,写入也会变得更加困难,因为您将面对符合代码提供和期望的"阻抗不匹配".
reintrepret_cast
?还是做指针运算?还是其他这样的黑客?"事实上,如果你把它放在心上,你就可以编写令人讨厌的代码,尽管对指南很好.但:
你很少这样做(在代码中的位置,不一定是执行时间的分数)
你只会故意这样做,而不是偶然.
这样做将在符合指南的代码库中脱颖而出.
这是一种代码,你可以用另一种语言绕过GC.
如果您是C++库开发人员,那么您确实编写了涉及原始指针的不安全代码,并且您需要仔细和负责任地编写代码 - 但这些代码是由专家编写的自包含代码(更重要的是,由专家审查).
所以,就像Bjarne说的那样:一般来说,没有动力收集垃圾,因为你们都要确保不要产生垃圾.GC正在成为C++的一个问题.
这并不是说当您想要使用自定义分配和取消分配策略时,GC对某些特定应用程序来说不是一个有趣的问题.对于那些您需要自定义分配和取消分配的人,而不是语言级别的GC.