为什么C ++没有垃圾回收器?


269

首先,因为垃圾收集的优点,所以我没有问这个问题。我提出这个问题的主要原因是,我确实知道Bjarne Stroustrup曾说过C ++将在某个时间点具有垃圾收集器。

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

交叉链接:

为了澄清起见,我理解C ++首次创建时没有垃圾收集器的原因。我想知道为什么不能添加收集器。


26
这是仇恨者总是提出的有关C ++的十大神话之一。垃圾回收不是“内置”的,但是有几种简单的方法可以使用C ++来实现。发表评论,因为其他人已经比我在下面的回答更好:)
davr

5
但这就是不内置的全部意义,您必须自己做。从高到低的真实性:内置,图书馆,自制。我自己使用C ++,因此绝对不会讨厌它,因为它是世界上最好的语言。但是动态内存管理很痛苦。
QBziZ

4
@Davr-我不是C ++的仇恨者,我甚至也不试图争辩C ++需要垃圾收集器。我之所以问是因为,我知道Bjarne Stroustrup曾经说过将要添加它,并且很好奇不执行它的原因是什么。
杰森·贝克

1
本文来自Dobbs的Boehm Collector for C和C ++描述了一种开源垃圾收集器,该垃圾收集器可以与C和C ++一起使用。它讨论了将垃圾收集器与C ++析构函数以及C标准库一起使用会引起的一些问题。
理查德·钱伯斯

1
@rogerdpack:到目前为止,它还没有那么有用(请参阅我的回答...),因此实现不太可能投资拥有一个。
einpoklum

Answers:


159

可以添加隐式垃圾回收,但是并没有成功。可能不仅是由于实施方面的复杂性,还因为人们没有足够快地达成普遍共识。

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并不如他所愿。


71
我会成为一个仇敌,如果C ++强制垃圾收集在我身上!人们为什么不能使用smart_ptr's?您将如何使用垃圾收集器进行低级Unix样式的分叉?其他因素也会受到影响,例如穿线。Python之所以具有全局解释器锁,主要是因为它具有垃圾回收功能(请参见Cython)。请将其放在C / C ++中,谢谢。
unixman83 '04

26
@ unixman83:引用计数垃圾回收(即std::shared_ptr)的主要问题是循环引用,这会导致内存泄漏。因此,您必须谨慎使用std::weak_ptr以打破周期,这很麻烦。标记和扫描样式GC不会出现此问题。线程/派生与垃圾回收之间没有固有的不兼容性。Java和C#都具有高性能的抢占式多线程和垃圾回收器。实时应用程序和垃圾收集器存在一些问题,因为大多数垃圾收集器必须停止运行。
Andrew Tomazos

9
“参考的主要问题计数的垃圾收集(即std::shared_ptr)是循环引用”和可怕的表现是具有讽刺意味,因为更好的性能,通常是用C ++ ...的理由flyingfrogblog.blogspot.co.uk/2011/01/...
乔恩Harrop 2013年

11
“您将如何进行低级Unix样式分支”。使用GC语言(例如OCaml)已经进行了大约20年或更长时间。
乔恩·哈罗普

9
“ Python之所以拥有全局解释器锁,主要是因为它具有垃圾回收功能”。斯特劳曼的论点。Java和.NET都具有GC,但都没有全局锁。
乔恩·哈罗普

149

在这里增加辩论。

垃圾回收存在一些已知问题,对它们的了解有助于理解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的语言有两种解决方法:

  • 当堆栈分配足够时不要使用GC:通常是为了解决性能问题,但是在我们的情况下,这确实有用,因为范围定义了生命周期
  • using构造...但是它是显式的(弱)RAII,而在C ++中,RAII是隐式的,因此用户不能不经意间就犯错误(通过省略using关键字)

3.智能指针

智能指针通常作为处理内存中的银弹出现C++。我常常听到:由于我们有智能指针,所以我们毕竟不需要GC。

一个再错不过了。

智能指针确实有帮助:auto_ptrunique_ptr使用RAII概念,的确非常有用。它们是如此简单,您可以很容易地自己编写它们。

但是,当需要共享所有权时,它将变得更加困难:您可能会在多个线程之间共享,并且计数处理存在一些细微的问题。因此,自然会走向shared_ptr

太好了,毕竟这就是Boost的目的,但这不是万灵丹。实际上,主要的问题shared_ptr是它模拟了由GC实施的GC,Reference Counting但是您需要自己全部实施循环检测... Urg

当然,这很weak_ptr麻烦,但是尽管如此,shared_ptr由于使用了这些周期,但不幸的是我已经看到内存泄漏了……而且当您处于多线程环境中时,这很难检测!

4.解决方案是什么?

没有万灵药,但与往常一样,这绝对是可行的。在没有GC的情况下,需要明确所有权:

  • 如果可能的话,宁愿在一个给定的时间只有一个所有者
  • 如果没有,请确保您的类图没有与所有权有关的任何周期,并通过巧妙地应用 weak_ptr

所以确实,拥有GC很棒。。。但这不是小问题。同时,我们只需要卷起袖子。


2
我希望我能接受两个答案!太好了 值得一提的是,就性能而言,在单独的线程中运行的GC实际上很常见(在Java和.Net中使用)。当然,在嵌入式系统中可能不可接受。
杰森·贝克

14
只有两种类型?如何“抄袭收藏家”?世代收藏家?各种并发收集器(包括Baker的硬实时跑步机)?各种混合收藏家?伙计,这个领域行业的无知有时使我惊讶。
只是我的正确观点2010年

12
我是说只有两种类型吗?我说有2个被广泛部署。据我所知,Python,Java和C#现在都使用Mark和Sweep算法(Java曾经有一个引用计数算法)。更精确地说,在我看来,比C#在短周期中使用Generational GC,在大周期中使用Mark And Sweep和在复制中消除内存碎片更容易。尽管我认为算法的核心是Mark And Sweep。您知道使用其他技术的主流语言吗?我总是很高兴学习。
Matthieu M.

3
您只是命名了一种使用三种语言的主流语言。
只是我的正确观点2010年

3
主要区别在于,世代GC和增量GC无需停止工作,而可以通过在访问GC指针时偶尔执行树遍历的迭代来使它们在单线程系统上工作,而不会产生过多开销。可以通过新节点的数量以及收集需求的基本预测来确定)。您可以通过在代码中包含有关节点创建/修改发生位置的数据来进一步提高GC的效率,这可以使您改善预测,并免费获得Escape Analysis。
Keldon Alleyne '04年

56

什么样的?是否应针对嵌入式洗衣机控制器,手机,工作站或超级计算机进行优化?
它应该优先考虑GUI响应性或服务器加载吗?
应该使用大量内存还是大量CPU?

C / c ++用于太多不同的情况。我怀疑诸如boost智能指针之类的东西对于大多数用户来说已经足够了

编辑-自动垃圾收集器并不是性能的问题(您可以随时购买更多服务器),而只是性能的问题。
不知道GC何时启动,就像聘用一名麻醉师的飞行员一样,在大多数情况下,他们都很棒-但当您真正需要响应时!


6
我绝对明白您的意思,但是我不得不问:Java是否在几乎相同的应用程序中使用?
杰森·贝克

35
不能。Java不适合高性能应用程序,原因很简单,因为Java不能提供与C ++相同程度的性能保证。因此,您会在手机中找到它,但不会在手机开关或超级计算机中找到它。
Zathrus

11
您总是可以购买更多服务器,但不能总是为客户的手机购买更多的CPU CPU!
Crashworks 2010年

8
Java在CPU效率方面取得了很多性能赶超。真正棘手的问题是内存使用情况,Java本质上比C ++的内存效率低。效率低下是由于它是垃圾收集。垃圾回收不能兼顾快速和高效的内存,如果您研究GC算法的工作速度,这一事实将变得显而易见。
Nate CK 2010年

2
@Zathrus java可以在优化jit的吞吐量b / c上获胜,尽管不是延迟(布尔实时),当然也不是内存占用。
gtrak

34

C ++没有内置垃圾回收的最大原因之一是,让垃圾回收与析构函数很好地配合确实非常困难。据我所知,还没有人真正知道如何彻底解决它。有很多问题要处理:

  • 确定对象的生存期(引用计数为您提供了这一点,但是GC却没有。尽管可能没什么大不了的)。
  • 如果在垃圾回收对象时析构函数抛出该怎么办?大多数语言都忽略了此异常,因为实际上没有捕获块可以将其传输到该异常,但这对于C ++来说可能不是可接受的解决方案。
  • 如何启用/禁用它?自然地,这可能是编译时的决定,但为GC编写的代码与为NOT GC编写的代码将有很大的不同,并且可能不兼容。您如何调和这一点?

这些只是面临的一些问题。


17
GC和析构函数是一个已解决的问题,这与Bjarne达成了很好的合作。析构函数不在GC期间运行,因为那不是GC的重点。存在于C ++中的GC可以创建无限内存的概念,而不是无限其他资源的概念。
MSalters

2
如果析构函数不运行,那将完全改变语言的语义。我猜至少您需要一个新的关键字“ gcnew”或类似的东西,以便您明确允许该对象进行GC处理(因此,不应使用它包装内存以外的资源)。
格雷格·罗杰斯

7
这是一个虚假的论点。由于C ++具有显式的内存管理,因此您需要弄清楚何时必须释放每个对象。使用GC,情况不会更糟。而是将问题简化为找出何时释放某些对象,即那些在删除时需要特别考虑的对象。使用Java和C#进行编程的经验表明,绝大多数对象不需要特别考虑,可以放心地交给GC。事实证明,C ++中析构函数的主要功能之一是释放子对象,GC会自动为您处理这些子对象。
Nate CK 2010年

2
@ NateC-K:与非GC(也许最大的事情)相比,GC有所改进的是,可靠的GC系统能够确保只要引用存在,每个引用将继续指向同一对象。调用Dispose对象可能会使它变得不稳定,但是在对象存活时指向该对象的引用将在它死后继续这样做。相比之下,在非GC系统中,可以在存在引用的情况下删除对象,并且如果使用这些引用之一,则对破坏的危害几乎没有任何限制。
supercat

22

尽管这是一个古老的问题,但仍然有一个我完全看不到的问题:几乎不可能指定垃圾回收。

特别是,C ++标准非常谨慎地从外部可观察到的行为而不是实现如何实现该行为的角度指定语言。在垃圾回收的情况下,然而,几乎没有外部观察到的行为。

垃圾回收的一般想法是,它应该做出合理的尝试来确保内存分配成功。不幸的是,即使您确实有垃圾收集器在运行,也几乎不可能保证任何内存分配都会成功。无论如何在某种程度上都是如此,但是对于C ++来说尤其如此,因为(可能)不可能使用复制收集器(或类似的东西)在收集周期内将对象移动到内存中。

如果您不能移动对象,则无法创建一个连续的内存空间来进行分配-这意味着您的堆(或免费存储,或您喜欢的任何名称)可以并且可能会,随着时间的流逝变得支离破碎。反过来,这可以防止分配成功,即使有更多的可用内存超出请求的数量。

尽管可能会提出一些保证,(实质上)说,如果您重复重复完全相同的分配模式,并且第一次成功,那么只要分配的内存,它将在后续迭代中继续成功在迭代之间变得不可访问。这是一个微弱的保证,实际上是没有用的,但是我看不出有任何合理的希望来加强它。

即使这样,它也比C ++所提出的要强大。在先前的提案 [警告:PDF](即得到下降)根本不保证任何事情。在28页的提案中,您从外部可观察到的行为得到的是一条(非规范性的)注释,说:

[注意:对于垃圾回收程序,高质量的托管实现应尝试最大程度地回收其无法访问的内存。—尾注]

至少对我而言,这引起了有关投资回报率的严重问题。我们将破坏现有的代码(没人知道确切多少,但肯定很多),对实现提出新的要求,并对代码施加新的限制,而我们得到的回报很可能根本不算什么?

即使充其量,我们所获得的程序都是基于Java进行的测试,以与现在相同的速度运行可能需要大约六倍的内存。更糟糕的是,垃圾收集从一开始就是Java的一部分-C ++对垃圾收集器施加了更多限制,使其几乎肯定会具有更差的成本/收益比(即使我们超出了提案所保证的范围并假定会有一些好处)。

我用数学的方式总结了这种情况:这是一个复杂的情况。正如任何数学家所知,复数有两个部分:实数和虚数。在我看来,这里的成本是真实的,但收益(至少大部分是虚构的)。


我假定即使为正确操作必须指定所有对象都必须删除,并且只有已删除的对象才有资格进行收集,编译器对引用跟踪垃圾收集的支持仍然有用,因为这种语言可以确保使用已删除的指针(引用)将被确保捕获,而不是导致未定义行为。
超级猫

2
即使在Java中,也没有真正指定GC可以执行任何有用的AFAIK。它可能需要free您(我的意思free是C语言的意思)。但是Java不能保证调用终结器或类似的东西。实际上,C ++在执行提交数据库写入,刷新文件句柄等操作方面比Java发挥更大的作用。Java声称具有“ GC”,但是Java开发人员必须close()时刻认真地进行调用,并且他们必须非常了解资源管理,请注意不要调用close()得太早或太晚。C ++使我们摆脱了困境。...(续)
亚伦·麦克戴德

2
..我刚才的评论不是要批评Java。我只是注意到“垃圾收集”这个词是一个很奇怪的术语-它的含义比人们想象的要少得多,因此,在不清楚其含义的情况下很难进行讨论。
亚伦·麦克戴德

@AaronMcDaid的确,GC根本无法帮助使用非内存资源。幸运的是,与内存相比,此类资源很少分配。而且,其中超过90%的对象可以通过分配它们的方法释放,因此try (Whatever w=...) {...}可以解决(忘记时会收到警告)。其余的也与RAII有关。close()始终“ 调用”意味着可能每万行执行一次,所以还算不错,而几乎在每条Java行上都分配了内存。
maaartinus

15

如果您想要自动垃圾收集,那么有一些不错的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兼容)

希望这可以帮助。


“其性能可与其他垃圾收集语言相媲美”。引用?
乔恩·哈罗普

1
我的链接断开了。我5年前写了这个答案。
Rayne 2013年

1
好的,我希望对这些声明进行一些独立的验证,即不要由Stroustrup或Boehm进行。:-)
乔恩·哈罗普

12

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比许多其他语言更短,更易读,更易教。


1
GC仅应用于不获取资源的对象(即,请“其他实体”另行通知,直到另行通知”)。如果Gadget不要求其他东西代表它做任何事情,那么如果delete删除了对Java毫无意义的语句,则原始代码在Java中将是绝对安全的。
supercat

@supercat,带有无聊析构函数的对象很有趣。(我没有定义“无聊”,但基本上不需要释放内存之外的析构函数)。单个编译器可能shared_ptr<T>T“无聊” 时特别对待。它可以决定不实际管理该类型的引用计数器,而是使用GC。这将允许使用GC,而无需开发人员注意。对于A,shared_ptr可以简单地将A 视为GC指针T。但是这样做有局限性,它会使许多程序变慢。
亚伦·麦克戴德

一个好的类型系统对于GC和RAII管理的堆对象应该具有不同的类型,因为某些使用模式对一个使用很好,而对另一个使用则很差。在.NET或Java中,声明string1=string2;将执行非常迅速,无论字符串的长度(它的字面意思无非寄存器加载和登记店铺),并且不需要任何锁定,以确保如果上述声明的同时执行string2IS被写入时,string1将保留旧值或新值,并且没有未定义行为)。
supercat 2015年

在C ++中,a的分配shared_ptr<String>需要大量的后台同步,String如果同时读取和写入变量,则a的分配可能会表现得很奇怪。人们想同时写和读的String情况并不是很常见,但如果例如某些代码希望使正在进行的状态报告可用于其他线程,则可能发生。在.NET和Java中,这样的事情“有效”。
supercat 2015年

1
@curiousguy没有任何改变,除非采取正确的预防措施,否则Java仍允许构造函数完成后立即调用终结器。这是一个真实的示例:“ finalize()在Java 8中调用了高度可访问的对象 ”。结论是永远不要使用此功能,几乎所有人都同意这是该语言的历史设计错误。当我们遵循该建议时,该语言将提供我们喜欢的确定性。
霍尔格

11

因为现代C ++不需要垃圾回收。

Bjarne Stroustrup 对此问题的常见问题解答说

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


对于当前编写的代码(C ++ 17和遵循官方 Core Guidelines)的情况如下:

  • 大多数与内存所有权相关的代码都在库中(尤其是那些提供容器的库)。
  • 涉及内存所有权的大多数代码使用都遵循RAII 模式,因此会在构造时进行分配,而在销毁时进行释放,这会在退出分配内容的范围时发生。
  • 没有直接显式分配或取消分配内存
  • 原始指针不拥有内存(如果您已遵循准则),因此不能通过传递原始指针来泄漏。
  • 如果您想知道如何在内存中传递值序列的起始地址-您将使用span来完成 ; 无需原始指针。
  • 如果您确实需要一个拥有的“指针”,则可以使用C ++的 标准库智能指针 -它们不会泄漏,而且效率很高(尽管ABI可以阻止这种情况)。或者,您可以使用“所有者指针”跨范围边界传递所有权。这些不常见,必须明确使用;但是在采用时-它们允许对泄漏进行良好的静态检查。

“哦,是吗?那...

...如果我只是像过去那样编写C ++来编写代码?”

确实,您可以不理会所有准则,而编写泄漏的应用程序代码-它会像往常一样编译和运行(和泄漏)。

但这不是“只是不做那件事”的情况,在这种情况下,期望开发人员是有道德的,并能进行很多自我控制。编写不合格的代码并不简单,编写起来也不快,性能也不更好。逐渐地,编写它也将变得更加困难,因为您将面临越来越多的“阻抗失配”,这与符合规范的代码所提供和期望的内容不符。

...如果我reintrepret_cast呢?还是做复杂的指针算术?还是其他此类黑客?”

确实,如果您下定决心,尽管编写指南得当,但您仍可以编写使事情变得混乱的代码。但:

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

...图书馆发展?”

如果您是C ++库开发人员,那么您确实会编写涉及原始指针的不安全代码,并且需要认真负责地进行编码-但这是专家编写的独立代码(更重要的是,经过专家审查)。


因此,就像Bjarne所说的那样:基本上没有收集垃圾的动机,因为大家都确保不要产生垃圾。使用C ++,GC不再是问题。

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


好吧,如果您正在研磨字符串,它确实(需要GC)..想象一下,您正在逐步构建大的字符串数组(认为数百兆字节),然后处理并重建为不同的长度,删除未使用的长度,组合其他长度等等。知道,因为我不得不改用高级语言来应对。(当然,您也可以构建自己的GC)。
www-0av-Com

2
@ user1863152:在这种情况下,自定义分配器将很有用。它仍然不需要使用语言集成的GC ...
einpoklum

对einpoklum:是的。这只是课程的选择。我的要求是处理动态变化的加仑运输旅客信息。有趣的主题..真正归结为软件哲学。
www-0av-Com

正如Java和.NET世界所发现的,GC最终存在一个巨大的问题-它无法扩展。当您像今天使用任何非平凡的软件那样拥有数十亿个活动对象的内存时,您将必须开始编写代码以将内容隐藏在GC中。在Java和.NET中拥有GC是一种负担。
Zach Saw

10

C ++背后的想法是,您不会为不使用的功能付出任何性能影响。因此,添加垃圾回收将意味着让某些程序像C一样直接在硬件上运行,而某些程序则在某种运行时虚拟机中运行。

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


2
我认为GC不需要VM。编译器可以向所有指针操作添加代码以更新全局状态,而一个单独的线程在后台运行,并根据需要删除对象。
user83255,2009年

3
我同意。您不需要虚拟机,但是第二步开始有一些东西可以像在后台那样为您管理内存,我的感觉是,您已经离开了实际的“电线”,并遇到了虚拟机情况。
Uri


4

原始C语言背后的基本原理之一是内存由一系列字节组成,并且代码仅需关心这些字节在使用时的确切含义。现代C允许编译器施加其他限制,但是C包括(并且C ++保留了)以下功能:将指针分解为字节序列,将包含相同值的任何字节序列组合为指针,然后使用该指针访问先前的对象。

尽管该功能在某些应用程序中可能是有用的,甚至是必不可少的,但是包含该功能的语言在支持任何有用且可靠的垃圾收集方面的能力将非常有限。如果编译器不了解组成指针的位所完成的所有操作,则它将无法知道宇宙中是否存在足以重建指针的信息。因为有可能以一种即使计算机知道信息也无法访问的方式来存储信息(例如,组成指针的字节可能已经在屏幕上显示了足够长的时间,以便有人编写)将它们放到一张纸上),计算机实际上不可能知道将来是否可以使用指针。

许多垃圾收集框架的一个有趣的怪癖是,对象引用不是由其中包含的位模式定义的,而是由对象引用中保留的位与其他地方保存的其他信息之间的关系定义的。在C和C ++中,如果存储在指针中的位模式标识一个对象,则该位模式将标识该对象,直到该对象被明确销毁为止。在典型的GC系统中,对象可能在某个时刻由位模式0x1234ABCD表示,但是下一个GC周期可能会将对0x1234ABCD的所有引用替换为对0x4321BABE的引用,因此该对象将由后一种模式表示。即使要显示与对象引用关联的位模式,然后再从键盘读回它,


这是一个很好的观点,我最近才从指针中窃取了一些位,因为否则将会有愚蠢的高速缓存未命中。

@PasserBy:我想知道有多少使用64位指针的应用程序会受益于将缩放的32位指针用作对象引用,还是将几乎所有内容都保留在4GiB的地址空间中,以及使用特殊对象从高位存储/检索数据?速度超越?机器具有足够的RAM,以至于64位指针的RAM消耗可能无关紧要,除非它们吞噬的缓存是32位指针的两倍。
超级猫

3

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

如果自动将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 ++代码将不起作用。


3

简短的回答:我们不知道如何有效地进行垃圾收集(时间和空间开销较小),并且始终正确(在所有可能的情况下)。

长答案:就像C一样,C ++是一种系统语言。这意味着在编写系统代码(例如操作系统)时使用它。换句话说,与C一样,C ++被设计为具有最佳性能为主要目标。该语言的标准不会添加任何可能妨碍性能目标的功能。

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

最后,如果控制(硬件,详细信息等)和性能(时间,空间,功率等)不是主要约束,则C ++不是编写工具。其他语言可能会更好,并且会提供更多的[隐藏]运行时管理,并具有必要的开销。


3

当我们将C ++与Java进行比较时,我们发现C ++在设计时并未考虑隐式垃圾回收,而Java是。

在C-Style中使用任意指针之类的东西不仅不利于GC实现,而且还会破坏大量C ++-legacy-code的向后兼容性。

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

总而言之:是的,可以将垃圾收集添加到C ++,但是为了连续性,最好不要这样做。


1
释放内存和运行析构函数是完全独立的问题。(Java没有析构函数,即PITA。)GC释放内存,但不运行dtor。
curiousguy

0

主要有两个原因:

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

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


有许多(较新的)算法在没有垃圾回收的情况下固有地难以实现。时间流逝。创新还来自与(垃圾收集)高级语言非常匹配的新见解。尝试将其中任何一个移植回免费的GC免费C ++,您会注意到路上遇到的困难。(我知道我应该举一些例子,但是我现在有点着急。很抱歉。我现在可以想到的一个问题是围绕持久性数据结构,在这种情况下引用计数将无效。)
BitTickler '18

0

强加垃圾回收实际上是从低级到高级的范式转换。

如果您看一看用垃圾回收语言处理字符串的方式,您会发现它们仅允许高级字符串操作功能,并且不允许二进制访问字符串。简而言之,所有字符串函数都将首先检查指针以查看字符串在哪里,即使您仅绘制一个字节也是如此。因此,如果您要执行一个循环,以带有垃圾回收的语言处理字符串中的每个字节,则该循环必须计算每次迭代的基本位置和偏移量,因为它无法知道字符串何时移动。然后,您必须考虑堆,堆栈,线程等。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.