为什么 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个回答

5
原始的C语言背后的一个基本原则是,内存由一系列字节组成,代码只需要关心这些字节在使用时的确切含义。现代C允许编译器施加附加限制,但C包括并且C++保留了将指针分解为一系列字节,将包含相同值的任何字节序列组装成指针,然后使用该指针访问先前对象的能力。
虽然这种能力在某些类型的应用程序中可能非常有用或甚至不可或缺,但包括这种能力的语言在支持任何有用且可靠的垃圾收集方面的能力将非常有限。如果编译器不知道指针所构成的位已经被做了什么处理,它将无法知道足以重构指针的信息是否存在于宇宙的某个地方。由于即使计算机知道它们的存在方式(例如,构成指针的字节可能已经显示在屏幕上长时间,以便有人将它们写在纸上),这些信息也可能以计算机无法访问的方式存储,因此计算机可能无法确定指针是否可能在未来被使用。
许多垃圾收集框架的有趣特点是,对象引用不是由其中包含的位模式定义的,而是由对象引用中持有的位与其他地方持有的信息之间的关系来定义的。在C和C++中,如果指针中存储的位模式标识一个对象,那么该位模式将标识该对象,直到显式销毁该对象为止。在典型的GC系统中,一个对象可能会在某个时刻表示为0x1234ABCD的位模式,但下一个GC周期可能会用0x4321BABE替换所有对0x1234ABCD的引用,此后该对象将由后者模式表示。即使显示与对象引用关联的位模式,然后稍后从键盘上读取它,也不应期望相同的位模式可用于标识相同的对象(或任何对象)。

这是一个非常好的观点,我最近从我的指针中窃取了一些位,否则会有愚蠢的缓存未命中。 - Passer By
@路人甲:我想知道有多少使用64位指针的应用程序可以从使用缩放的32位指针作为对象引用或将几乎所有内容保留在4GiB地址空间中并使用特殊对象从高速存储器中存储/检索数据中受益?由于机器拥有足够的RAM,因此64位指针的RAM消耗可能并不重要,* 除非它们比32位指针多吞下两倍的缓存。 - supercat

4

简短回答:

我们不知道如何高效地(带有最小的时间和空间开销)并且在所有可能的情况下正确地进行垃圾回收。

详细回答:

与C语言一样,C++是一种系统语言;这意味着当您编写系统代码时,例如操作系统时使用它。换句话说,C++的设计与C一样,以最佳性能为主要目标。该语言的标准不会添加任何可能会妨碍性能目标的功能。

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

最后,如果控制(硬件、细节等)和性能(时间、空间、功率等)不是主要约束条件,则C++不是正确的工具。其他语言可能更好地服务并提供更多(隐藏的)运行时管理,具有必要的开销。


最佳答案。它是唯一指向主要原因和根本原因的答案。 - Sohail Si

3
当我们将C++与Java进行比较时,我们发现C++并没有为隐式垃圾收集而设计,而Java则是。在C风格中具有任意指针等内容不仅对于垃圾回收实现不好,同时还会破坏大量的C++旧代码的向后兼容性。此外,C++是一种旨在作为独立可执行文件运行而不是具有复杂运行时环境的语言。总之,虽然可能可以将垃圾收集添加到C++中,但出于连续性考虑最好不要这样做。

1
释放内存和运行析构函数是两个完全不同的问题。(Java 没有析构函数,这很麻烦。)垃圾回收器负责释放内存,而不是运行析构函数。 - curiousguy

3
所有的技术讲解都过于复杂了。
如果你将GC放入C++中,使所有内存自动处理,那么考虑一下像Web浏览器这样的东西。Web浏览器必须加载完整的Web文档并运行Web脚本。您可以在文档树中存储Web脚本变量。在具有许多打开选项卡的浏览器中的大型文档中,这意味着每次GC必须进行完全收集时,它还必须扫描所有文档元素。
在大多数计算机上,这意味着会发生页面错误。因此,回答问题的主要原因是会发生页面错误。当您的PC开始进行大量磁盘访问时,您将知道这一点。这是因为GC必须触及大量内存以证明无效指针。当您拥有真正使用大量内存的应用程序时,每次收集时必须扫描所有对象会造成混乱,因为会发生页面错误。页面错误是指需要从磁盘读回虚拟内存的情况。
因此,正确的解决方案是将应用程序分成需要GC和不需要GC的部分。在上面的Web浏览器示例中,如果文档树是通过malloc分配的,但JavaScript是通过GC运行的,则每次GC启动时,它只扫描内存的一小部分,并且文档树的所有PAGED OUT元素不需要被分页回来。
为了进一步理解这个问题,请查阅有关虚拟内存及其在计算机中的实现的信息。它都是关于程序可以使用2GB,而实际上并没有那么多RAM的事实。在具有2GB RAM的32位系统的现代计算机上,如果只运行一个程序,则不会出现太大问题。
作为另一个例子,考虑必须跟踪所有对象的完全收集。首先,您必须扫描通过根可达的所有对象。第二步扫描第1步中可见的所有对象。然后扫描等待销毁者。然后再次转到所有页面并关闭所有不可见对象。这意味着许多页面可能会多次交换出和交换入。
因此,我的简短回答是,由于触及所有内存会导致页面错误的数量,因此对于程序中的所有对象进行完整GC是不可行的,因此程序员必须将GC视为脚本和数据库工作等辅助工具,但使用手动内存管理进行正常操作。
当然,另一个非常重要的原因是全局变量。为了使收集器知道全局变量指针在GC中,它需要特定的关键字,因此现有的C++代码将无法工作。

0

主要有两个原因:

  1. 因为它不需要(在我看来)
  2. 因为它与 RAII 几乎不兼容,而 RAII 是 C++ 的基石

C++ 已经提供了手动内存管理、堆栈分配、RAII、容器、自动指针、智能指针等等。这应该足够了。垃圾回收器是为懒惰的程序员准备的,他们不想花费 5 分钟思考谁应该拥有哪些对象或者资源何时应该被释放。这不是我们在 C++ 中做事情的方式。


有许多(较新的)算法本质上很难在没有垃圾回收的情况下实现。时间在流逝,创新也来自于与(垃圾回收)高级语言相匹配的新见解。试着将它们中的任何一个回溯到无GC的C++中,你会注意到路上的颠簸。(我知道我应该举例子,但我现在有点赶。抱歉。我现在能想到的一个例子是围绕持久数据结构,其中引用计数不起作用。) - BitTickler

0

实施垃圾回收确实是从低级到高级范式的转变。

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


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