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

304

首先,我提出这个问题并不是因为垃圾回收的优点。我提问的主要原因是我知道Bjarne Stroustrup曾经说过C++在某个时间点会有一个垃圾回收器。

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

跨链接:

仅仅为了澄清一下,我理解C++在创立时没有垃圾回收器的原因。我想知道的是为什么不能添加回收器。


35
这是有关C++的十大谣言之一,总是被那些反对者提出来。虽然C++没有“内置”的垃圾收集机制,但有几种简单的方法可以实现它。我在此发表评论,因为其他人已经比我回答得更好了 :) - davr
6
但这正是不内置的全部意义,你必须自己完成。可靠性从高到低:内置、库、自制。 我自己使用C++,绝对不是因为它是世界上最好的语言而讨厌它。但动态内存管理真是一件痛苦的事情。 - QBziZ
4
@Davr - 我不是C++的反对者,也没有试图争论C++需要垃圾回收器。我问这个问题是因为我知道Bjarne Stroustrup曾经说过它将被添加,并且只是好奇为什么没有实现它的原因是什么。 - Jason Baker
1
本文来自Dr. Dobbs的C和C++ Boehm收集器描述了一种开源垃圾收集器,可用于C和C++。它讨论了使用带有C++析构函数以及C标准库的垃圾收集器时出现的一些问题。 - Richard Chambers
1
@rogerdpack:但是现在它并不是那么有用(请看我的回答...),所以实现它的可能性很小。 - einpoklum
显示剩余3条评论
16个回答

180

隐式垃圾回收可能已经被加入,但它没有被采纳。这可能不仅是由于实现上的复杂性,还因为人们无法快速达成共识。

以下是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应该拥有更多的东西,但实际上并没有。


82
如果C++强制要求我使用垃圾回收机制,我会成为一个反对者!为什么人们不能使用"智能指针"?如果有垃圾回收机制,如何进行低级别的Unix风格分叉操作?其他事情也会受到影响,比如线程。Python由于其垃圾回收机制(见Cython)而有其“全局解释器锁定”。请将其从C / C++中排除在外,谢谢。 - unixman83
39
主要问题在于引用计数垃圾回收(即std::shared_ptr)存在循环引用,会导致内存泄漏。因此,您必须仔细使用std::weak_ptr来打破循环引用,但这很麻烦。标记清除式GC没有这个问题。线程和进程间并不与垃圾回收不兼容。Java和C#都具有高性能的抢占式多线程和垃圾收集器。对于实时应用程序和垃圾回收器,存在一些问题,因为大多数垃圾回收器必须停止运行才能进行垃圾回收。 - Andrew Tomazos
12
参考计数垃圾回收(例如 std::shared_ptr)的主要问题是循环引用和糟糕的性能,这很讽刺,因为更好的性能通常是使用 C++ 的理由之一。请参见:http://flyingfrogblog.blogspot.co.uk/2011/01/boosts-sharedptr-up-to-10-slower-than.html - J D
21
你想知道如何进行低级Unix风格的分叉。和OCaml等带有垃圾回收的语言做法一样,已经使用了约20年或更长时间。 - J D
17
Python有全局解释器锁,主要是因为它的垃圾回收机制。草人论证。Java和.NET都有垃圾回收机制,但都没有全局锁。 - J D
显示剩余27条评论

166

在这里补充一下讨论。

垃圾回收存在已知的问题,理解这些问题有助于理解为什么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的语言有两个解决方法:

  • 当栈分配足够使用时,不要使用GC:通常是出于性能方面的考虑,在我们的情况下确实有所帮助,因为作用域定义了生命周期。
  • using构造…但它是显式(弱)RAII,而在C++中,RAII是隐式的,以便用户不会无意中犯错误(省略using关键字)。

3. 智能指针

在C++中,智能指针经常被视为处理内存的银弹。我经常听到这样的说法:毕竟我们有了智能指针,所以我们不需要GC。

但没有比这更错的想法了。

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

然而,当需要共享所有权时,问题就变得更加困难:可能需要在多个线程间共享,而处理计数的一些微妙问题。因此,人们自然会使用shared_ptr

很好,这正是Boost的目的,但这并不是万能药。实际上,shared_ptr 的主要问题在于它模拟了一个通过引用计数实现的 GC,但你需要自己实现循环检测...累人。

当然,有这种东西叫做 weak_ptr,但我不幸地发现,尽管使用了shared_ptr,仍会因为这些循环而导致内存泄漏...而且当你处于多线程环境中时,很难检测到!

4. 解决方案?

没有银弹,但通常情况下是可行的。在没有垃圾回收机制的情况下,我们需要明确所有权:

  • 如果可能,优先考虑在给定时间内只有单个所有者
  • 如果不行,请确保您的类图不具有涉及所有权的循环,并使用微妙的应用weak_ptr来打破它们

所以,确实希望有一个 GC... 但这不是简单的问题。与此同时,我们只需挽起袖子。


16
只有两种类型?拷贝收集器呢?分代收集器呢?各种并发收集器(包括Baker's硬实时跑步机收集器)?不同的混合收集器呢?唉,这个领域行业中的纯粹无知有时让我感到惊讶。 - JUST MY correct OPINION
14
我说过只有两种类型吗?我说有两种类型被广泛应用。据我所知,现在Python、Java和C#都使用标记-清除算法(Java曾经使用引用计数算法)。更加精确地说,似乎C#在次要循环中使用生成式GC,主要循环中使用标记-清除,采用复制技术来避免内存碎片化;虽然我认为算法的核心是标记-清除。你知道还有哪些主流语言使用其他技术吗?我总是乐意学习。 - Matthieu M.
3
你刚刚提到了一种使用了三种语言的主流语言。 - JUST MY correct OPINION
3
主要区别在于分代GC和增量GC不需要停止整个系统就能工作,而且你可以在单线程系统上使用它们而不会有太多开销,只需在访问GC指针时偶尔执行一次树遍历迭代(这个因素可以通过新增节点的数量来确定,以及对回收需求的基本预测)。你还可以通过包含有关节点创建/修改发生的代码位置的数据来进一步改进GC,这可以帮助你提高预测准确度,并且你还可以免费获得逃逸分析。 - Keldon Alleyne
2
@MatthieuM.:更进一步地说,假设一个程序重复使用“new”分配1,000个对象,用一些数据填充它们,将它们的地址转换为数字,在屏幕上显示1/60秒,然后在不执行“delete”的情况下放弃这些对象。稍后,程序允许用户键入一个数字,将其转换为地址,并在假定它表示有效对象的情况下使用它。如果用户输入的数字已被显示为对象的地址,则C ++标准要求该地址仍然有效。 - supercat
显示剩余14条评论

56

应该优化哪种类型的嵌入式洗衣机控制器、手机、工作站还是超级计算机?
应该优先考虑 GUI 的响应速度还是服务器加载?
它应该使用大量内存还是大量 CPU?

C/C++ 在许多不同的情况下被使用。 我猜像 Boost 智能指针这样的东西对大多数用户来说已经足够了。

编辑-自动垃圾回收器并非性能问题(您总是可以购买更多服务器),而是可预测性能问题。
不知道垃圾回收什么时候启动就像雇佣一个患有睡眠病的航空飞行员,大多数情况下他们都很棒,但当你真正需要响应性能时!


6
我完全理解你的观点,但我觉得有必要问一下:Java 不也被广泛应用于许多应用程序中吗? - Jason Baker
38
不,Java不适合高性能应用程序,原因很简单,它没有像C++那样具有同等程度的性能保证。因此,你会在手机中找到它,但不会在手机交换机或超级计算机中找到它。 - Zathrus
11
您总是可以购买更多的服务器,但您无法为已经在客户口袋中的手机购买更多的CPU! - Crashworks
11
Java在CPU效率方面已经做了很多性能追赶。真正棘手的问题是内存使用,Java在本质上比C++更耗费内存。而这种低效性源于其具有垃圾回收机制。垃圾回收不能同时又快又节省内存,这一事实在深入了解垃圾回收算法的工作原理后变得显而易见。 - Nate C-K
2
@Zathrus,864个核心不足以被认为是一台超级计算机吗?在实时交易中单线程每秒处理6百万订单不足以被认为是高性能吗?Java仍然存在一些缺点,但对于几乎所有任务来说都非常出色。 - maaartinus
显示剩余3条评论

35

C++没有内置垃圾回收机制的最大原因之一是:让垃圾回收与析构函数协同工作非常困难,据我所知,目前还没有完全解决这个问题的办法。以下是需要处理的许多问题:

  • 对象确定的生命周期(引用计数可以实现这一点,但垃圾回收不行。虽然这可能不是什么大问题)。
  • 如果在垃圾回收时对象的析构函数抛出异常会发生什么?大多数语言忽略了这个异常,因为没有捕获块可以传递它,但这对C++来说可能不是一个可接受的解决方案。
  • 如何启用/禁用垃圾回收?自然地它可能是一个编译时决定,但GC和非GC代码很不同,可能不兼容,如何解决这个问题?

这只是面临的一些问题。


17
GC和析构函数是一个已经解决的问题,通过Bjarne的巧妙绕过。析构函数不会在GC期间运行,因为这不是GC的目的。C++中的GC存在是为了创建无限“内存”的概念,而不是无限其他资源的概念。 - MSalters
4
如果析构函数不运行,那就完全改变了语言的语义。我猜至少你需要一个新的关键字“gcnew”或类似的东西,以便明确允许该对象被GC(因此您不应将其用于除内存之外的资源包装)。 - Greg Rogers
7
这是一个无效的论点。因为C++需要显式的内存管理,你需要找出每个对象何时需要被释放。而使用GC并不会更糟,相反,问题被简化为找出何时释放某些对象,即那些在删除时需要特殊考虑的对象。在Java和C#编程经验中发现,绝大多数对象不需要特殊考虑,可以安全地留给GC处理。事实证明,C++中析构函数的主要功能之一是释放子对象,而GC会自动处理这一部分。 - Nate C-K
2
@NateC-K:在GC和非GC之间改进的一件事情(也许是最大的)是一个坚实的GC系统能够保证只要引用存在,每个引用将继续指向相同的对象。在对象上调用“Dispose”可能会使其无法使用,但是当它还活着时指向该对象的引用将在其死后继续指向该对象。相比之下,在非GC系统中,对象可以在引用存在的情况下被删除,并且如果其中一个引用被使用,很少有任何限制可能造成的破坏。 - supercat

24
尽管问题很老,但仍有一个问题没有人解决:垃圾回收几乎不可能指定。特别地,C++标准非常小心地以外部可观察的行为来指定语言,而不是实现如何实现该行为。然而,在垃圾回收的情况下,实际上几乎没有外部可观察的行为。垃圾回收的一般思想是应该尽力保证内存分配将成功。不幸的是,即使你有一个垃圾收集器在运作,基本上不可能保证任何内存分配都会成功。这在任何情况下都是真实的,但在C++的情况下尤其如此,因为在收集周期内移动对象的拷贝收集器(或类似物品)(可能)无法使用。如果无法移动对象,则无法从中创建单个连续的内存空间以进行分配,这意味着您的堆(或自由存储区域或您喜欢称之为的任何内容)可能会随时间变得分散。反过来,即使有更多的空闲内存可用于请求的数量时,这也可能阻止分配成功。虽然可能会提出某些保证,即(基本上)如果您重复相同的分配模式,并且第一次成功,那么在后续迭代中它将继续成功,前提是分配的内存在迭代之间变得不可访问。这是如此微弱的保证,基本上没有用处,但我看不到任何合理的加强它的希望。
即便如此,它比C++的先前提议更为强大。之前的提议(警告:PDF)(已被放弃)根本没有保证任何东西。在28页的提案中,你所得到的对于外部可观察行为的内容只有一个(非规范性的)注释,内容如下:
[注意:对于垃圾收集程序,高质量的托管实现应尽可能地最大化回收的无法访问的内存。——注释结束]
至少对我来说,这引发了一个严重的投资回报问题。我们将打破现有的代码(没有人确切知道有多少,但肯定相当多),对实现施加新要求并对代码施加新限制,而我们所得到的回报很可能是什么都没有?
即使在最好的情况下,根据使用Java进行测试,我们所得到的程序可能需要运行6倍的内存才能以相同的速度运行。更糟糕的是,垃圾回收从一开始就是Java的一部分——C++对垃圾回收器的限制足够多,即使我们超出了提案的保证并假设会有一些好处,它几乎肯定会有更糟糕的成本/效益比率。
我会将情况数学化概括:这是一个复杂的情况。任何数学家都知道,复数有两个部分:实部和虚部。在我看来,我们在这里面临的是实际成本,但是收益(至少大部分)是想象出来的。

我认为,即使一个人指定了所有对象必须被删除才能正常运行,并且只有已被删除的对象才有资格进行收集,编译器对于引用跟踪垃圾回收的支持仍然是有用的,因为这样的语言可以确保使用已删除的指针(引用)将会被保证陷入陷阱,而不是导致未定义的行为。 - supercat
3
在Java中,据我所知,垃圾收集器并没有被明确定义为执行任何有用的操作。它可能会为你调用free(这里的free类比于C语言)。但是Java从不保证调用finalizers或类似的内容。事实上,相对于Java,C++在运行时需要更多地处理数据库写操作、刷新文件句柄等。虽然Java声称拥有“GC”,但Java开发人员必须一丝不苟地经常调用close(),并且他们必须非常注意资源管理,小心不要过早或过晚地调用close()。而C++让我们摆脱了这种束缚。...(续) - Aaron McDaid
2
我之前的评论并不是要批评Java。我只是观察到,“垃圾回收”这个术语非常奇怪 - 它的含义比人们想象的少得多,因此很难在不清楚其含义的情况下讨论它。 - Aaron McDaid
@AaronMcDaid确实,GC根本无法帮助非内存资源。幸运的是,与内存相比,这些资源分配得相当少。此外,超过90%的资源可以在分配它们的方法中释放,因此try(Whatever w = ...){...}解决了它(如果您忘记了,还会收到警告)。剩下的一些问题也存在RAII中。每次调用“close()”意味着每万行可能只有一次,所以并不那么糟糕,而几乎在每个Java行上都会分配内存。 - maaartinus

18

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

Bjarne Stroustrup在这个问题上的回答如下:

我不喜欢垃圾。我不喜欢乱扔东西。我的理想是通过不产生任何垃圾来消除需要垃圾收集器的需求。现在已经可能了。


对于现代C++(C++17及其以上版本,并遵循官方核心指南)编写的代码,情况如下:

  • 大多数与内存所有权相关的代码都在库中(尤其是提供容器的库)。
  • 大多数涉及内存所有权的代码使用CADRe或RAII 模式,因此分配在构造时完成,销毁在析构时完成,在分配某些内容的作用域退出时会发生。
  • 不会直接显式分配或释放内存
  • 原始指针不拥有内存(如果您遵循指南),因此您无法通过传递它们来泄漏。
  • 如果你想知道如何传递内存中值的序列的起始地址,可以并且应该使用span,这样就不需要使用原始指针了。当然,您仍然可以使用此类指针,只是它们不再拥有内存。
  • 如果你确实需要一个所有权指针,可以使用 C++ 的标准库智能指针 - 它们不会泄漏,并且效率相当高(尽管ABI可能会阻碍一些优化)。或者,你可以使用“所有权指针”跨作用域边界传递所有权。这些方法很少见,必须显式使用;但一旦采用,它们允许对泄漏进行良好的静态检查。
  • “是吗?那么......”

    “如果我只是按照我们过去编写 C++ 的方式来编写代码呢?”

    确实,您可以无视所有准则编写有泄漏的应用程序代码 - 它将编译并运行(并泄漏),与以往相同。

    但这不是一个“只是不要那样做”的情况,在这种情况下,开发人员被期望具有美德并自我控制;编写不符合规范的代码既不简单,也不更快,也不具有更好的性能。随着时间的推移,编写此类代码也将变得更加困难,因为您将面临与符合规范的代码提供和期望的“阻抗不匹配”。

    “如果我使用reinterpret_cast?或进行复杂的指针算术运算?或其他类似的黑客技巧?”

    确实,如果你下定决心,可以编写破坏规则的代码。但:

    1. 你很少这样做(指在代码中的位置上,而非执行时间的分数)
    2. 你只会故意这样做,而非偶然
    3. 这样做会在符合准则的代码库中脱颖而出。
  • 这种代码通常是在其他语言中绕过GC的。
  • ......库开发?"

    如果您是C++库开发人员,那么您需要编写涉及原始指针的不安全代码,并且需要小心、负责任地编码 - 但这些都是由专家编写(更重要的是,由专家审查)的自包含代码。


    所以,就像Bjarne所说:通常没有动力来收集垃圾,因为您几乎不会产生垃圾。对于C ++而言,GC已经成为一个非问题。

    这并不是说GC不是某些特定应用程序的有趣问题,当您想要使用自定义分配和解除分配策略时,您需要的是自定义分配和解除分配,而不是语言级别的GC。


    1
    如果你正在处理字符串,那么它确实需要垃圾回收。想象一下,你有大型的字符串数组(数百兆字节),你正在逐步构建它们,然后进行处理并重新构建成不同长度,删除未使用的部分,组合其他部分等等。我知道这是因为我不得不转向高级语言来应对这个问题。(当然,你也可以自己构建垃圾回收机制)。 - www-0av-Com
    2
    @user1863152: 这是一个需要自定义分配器的情况。但这并不必然需要语言内置的垃圾回收机制... - einpoklum
    对 einpoklum 说:没错,只是因人而异。我的要求是处理动态变化的运输乘客信息的加仑。非常迷人的主题... 真正关键的是软件哲学。 - www-0av-Com
    Java和.NET世界已经发现,GC存在一个巨大的问题 - 它无法扩展。当您拥有数十亿个实时对象在内存中,就像我们这些日子里使用任何非平凡软件一样,您将不得不开始编写代码来隐藏GC中的东西。在Java和.NET中使用GC是一种负担。 - Zach Saw
    @ZachSaw:有多少程序会在内存中拥有十亿个活动对象?你是说所有没有超过这个数量的程序都是微不足道的吗? - supercat
    显示剩余11条评论

    17
    如果你想要自动垃圾回收,C++有很好的商业和公共领域的垃圾回收器。对于适合使用垃圾回收的应用程序来说,C++是一种表现优异、与其他垃圾回收语言相比较有优势的垃圾回收语言。关于C++中的自动垃圾回收,请参见《C++程序设计语言(第4版)》。此外,还可以参考Hans-J. Boehm的C和C++垃圾回收站点(档案)。
    此外,C++支持编程技术,可以使内存管理变得安全和隐式,无需垃圾回收器。我认为垃圾回收是一种最后的选择,也是一种处理资源管理的不完美方式。这并不意味着它从来没有用处,只是在许多情况下有更好的方法。

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

    至于为什么它没有内置垃圾回收,如果我没记错的话,它是在GC成为“事物”之前发明的,而且我认为该语言由于多种原因(例如与C的向后兼容性)可能无法拥有GC。

    希望这可以帮到您。


    1
    性能方面与其他垃圾回收语言相比具有优势。引用出处? - J D
    1
    我的链接已经失效了。我是5年前写的这篇答案。 - Rayne
    1
    好的,我希望这些主张能得到一些独立的验证,即不是由Stroustrup或Boehm提供的。 :-) - J D

    14

    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”一样表现。易于预测,易于阅读和教学。一切都是“值”。有时候是大值,但是值比指针(或引用)更容易教授,因为你没有这种“远程操作”的事情。
    你创建的大多数对象仅在创建它们的函数中使用,并且可能作为输入传递给子函数。程序员在返回对象或在软件的广泛分离部分共享对象时不必考虑“内存管理”。
    作用域和生命周期很重要。大多数情况下,如果生命期与作用域相同,那么更容易理解和教授。当您需要不同的生命周期时,通过例如使用shared_ptr明确地在代码中表示。 (或通过值返回(大)对象,利用移动语义或unique_ptr)
    这似乎是一个效率问题。如果要从foo()返回Gadget,则C ++ 11的移动语义使返回大对象变得更加容易。只需编写Gadget foo(){...}就可以了,它会正常工作并快速工作。您不需要自己处理“&&”,只需按值返回东西,语言通常能够执行必要的优化。 (即使在C ++ 03之前,编译器也非常擅长避免不必要的复制。)
    正如Stroustrup在视频中的其他地方所说的(引述):“只有计算机科学家才会坚持复制对象,然后销毁原始对象。 (观众笑了)。为什么不直接将对象移动到新位置?这是人类(而不是计算机科学家)期望的。”
    当您可以保证只需要一个对象的副本时,更容易理解对象的生命周期。您可以选择所需的生命周期策略,如果需要,垃圾回收就在那里。但是,一旦理解了其他方法的好处,您会发现垃圾回收排在首选项列表的最底部。
    如果这对您不起作用,则可以使用unique_ptr,或者如果失败,则可以使用shared_ptr。使用C ++ 11编写的代码比许多其他语言在内存管理方面更短,易于阅读和教学。

    1
    GC只应用于不获取资源的对象(即要求其他实体代表其执行操作“直到另行通知”)。如果Gadget不再要求其他实体代表其执行操作,那么在Java中,如果删除无意义(对Java而言)的delete语句,则原始代码将是完全安全的。 - supercat
    @supercat,具有无聊析构函数的对象是有趣的(我没有定义“无聊”,但基本上是指除了释放内存之外,永远不需要调用析构函数的析构函数)。当T是“无聊”的时候,某个编译器可能会特殊处理shared_ptr<T>。它可以决定不为该类型实际管理引用计数器,而是使用GC。这将允许在开发人员不需要注意的情况下使用GC。对于适当的T,shared_ptr可以简单地被视为GC指针。但是这种方法存在局限性,并且会使许多程序变慢。 - Aaron McDaid
    在Java和.NET中,用于封装字符序列的最常见类型是“String”,它是一个不可变的引用类型,由于这是在Java中唯一能够提供实际值语义的东西,因此使用该类型,并且在.NET中只有这种类型可以在没有过多装箱的情况下提供值语义 [在.NET中,一个封装了“char []”的结构可能比堆对象更好,除了装箱问题之外]。为了比较,我会说应该将其与C ++中最有效地表示的任何类型进行比较... - supercat
    很抱歉要重复一遍,但你的 C++ String 是不是不可变的?你之前谈到了读和写,但对于一个不可变的 String 来说,写入是没有意义的。可以考虑使用类似 using String = const std::vector<char>; 的方式。在明确类型之前,我无法考虑其他问题。 - Aaron McDaid
    1
    @curiousguy 没有任何改变,除非你采取正确的预防措施,否则Java仍然允许在构造函数完成后立即调用finalizer。这里有一个现实生活中的例子:“在Java 8中调用强可达对象上的finalize()”。结论是永远不要使用这个功能,几乎每个人都认为这是语言历史上的设计错误。当我们遵循这个建议时,语言提供了我们所喜爱的确定性。 - Holger
    显示剩余7条评论

    11

    C++ 的设计理念是:如果你不使用某些功能,就不必为其支付性能上的代价。因此,添加垃圾回收机制意味着有些程序可以像 C 语言一样直接在硬件上运行,而有些则需要在运行时虚拟机中运行。

    并没有什么阻止你使用某种绑定到第三方垃圾回收机制的智能指针。我记得微软曾经在 COM 中做过类似的事情,但效果不太好。


    2
    我认为GC不需要虚拟机。编译器可以在所有指针操作中添加代码以更新全局状态,同时单独的线程在后台根据需要删除对象。 - user83255
    3
    我同意。你不需要虚拟机,但是一旦有类似于在后台为你管理内存的东西,我的感觉是你已经离开了实际的“电线”,并且进入了一种类似虚拟机的情况。 - Uri

    8

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接