C++中的垃圾回收 -- 为什么?

49
我听到很多人抱怨C++没有垃圾回收机制。我也听说C++标准委员会正在考虑将其添加到语言中。但我不明白它的意义...使用带有智能指针的RAII就可以消除对垃圾回收机制的需求,对吗?
我唯一接触过垃圾回收机制是在几台廉价的80年代家用电脑上,它会导致系统每隔一段时间卡顿几秒钟。我相信它现在已经得到改进,但你可以想象,那并没有给我留下很高的评价。
对于有经验的C++开发者,垃圾回收机制可以提供哪些优势呢?

你能描述一下什么是“使用智能指针的RAII”吗? - Craig Day
14
这是一个强大的C++习语,在C++界广为人知。如果你不了解,建议提出问题(或搜索,也许已经有答案了)。 - coppro
他的意思是,如果你严格遵循面向对象的编程思想,当对象超出作用域或没有更多引用时,你可以依赖于delete[]被调用,这应该会释放对象所持有的任何内存和资源。 - Matt J
16个回答

74

我听到很多人抱怨C++没有垃圾回收机制。

他们真的很可怜。

C++有RAII,而且我总是抱怨在垃圾收集语言中找不到RAII(或被削弱的RAII)。

垃圾回收能为有经验的C ++开发人员带来什么优势?

另一种工具。

Matt J 在他的文章中( Garbage Collection in C++ -- why?)写得很对:我们不需要 C++ 的特性,因为它们中的大部分都可以用 C 代码实现,我们也不需要 C 的特性,因为它们中的大部分都可以使用汇编代码实现,等等。C++必须进化。

作为一个开发人员:我不关心GC。我尝试过RAII和GC,并发现RAII要好得多。就像Greg Rogers在他的文章中 (Garbage Collection in C++ -- why?)所说的那样,在C ++中内存泄漏并不是很可怕(至少在真正使用C++时很少出现),不能因为RAII来取代GC而不顾及内存选择。GC具有非确定性的释放/终止,只是一种编写代码的方式,不关心特定内存选择。

这句话很重要:编写“只是不关心”的代码很重要。就像在C++ RAII中我们不需要关心资源释放,因为RAII为我们完成了它,或者对于对象初始化,因为构造函数为我们完成了它一样,有时仅仅编写代码而不必担心哪个内存的所有者、为此或那个代码片段需要什么样的指针(共享、弱等)是很重要的。C++似乎需要GC。(即使我个人没有看到它)

C++中良好使用GC的示例

有时在应用程序中会有“浮动数据”。想象一下树形结构的数据,但没有人真正“拥有”这些数据(也没有人真正关心何时它们将被销毁)。多个对象可以使用它,然后丢弃它。您希望它在没有人再使用它时自动释放。
C++ 的解决方法是使用智能指针。boost::shared_ptr 是一个不错的选择。所以每个数据块都由一个共享指针拥有。很好。但问题是当每个数据块可以引用另一个数据块时,你不能使用共享指针,因为它们使用引用计数器,无法支持循环引用(A 指向 B,而 B 又指向 A)。因此,您必须仔细考虑何时使用弱指针(boost::weak_ptr)和何时使用共享指针。
使用 GC 时,您只需使用树形结构的数据即可。
缺点是您不能关心“浮动数据”真正何时被销毁,只需知道它将被销毁。
总之,如果正确使用,并与 C++ 的当前语言习惯兼容,GC 将成为 C++ 中的又一种好工具。C++ 是一种多范式语言:添加 GC 可能会让某些 C++ 粉丝因叛变而哭泣,但最终这可能是一个好主意,并且我想 C++ 标准委员会不会让此类重要特性破坏语言,因此我们可以相信他们将进行必要的工作来启用正确的 C++ GC,而不会干扰 C++。像 C++ 中的所有功能一样,如果您不需要某个功能,请不要使用它,这将不会花费您任何东西。

5
“C++的RAII有限,可以更好”的例子有哪些?还有其他语言存在更好的RAII吗? - paercebal
7
更重要的是,你需要同时具备RAII和GC。一个不排除另一个。大多数从一开始就内置有GC的语言也有类似RAII的习惯用法。你可能认为自己不需要GC,但如果有机会获得更大的便利性和更高的生产力,谁能真正拒绝呢?普遍存在的GC让你以不同的方式设计和编写代码,从而提高了生产力。另一个经常被忽视的优点是,它通常也表现更好! - Daniel Earwicker
11
@Earwicker:我知道的GC主要语言(即非脚本非利基语言)是Java和C#。Java根本没有RAII,而C#的RAII远不能令人满意,尤其是从C++转过来的。但我们有一个共同的观点:如果我们负担得起,在一个内存分配在后台处理的语言中工作可以节省很多时间。 - paercebal
3
@Thomas Eding: 不,我的意思是“树”。(XML) DOM 是一种类似于树的结构,但每个节点通常除了指向其子节点的指针列表外,还有一个指向其父节点的指针(甚至可能有一个指向其文档节点的指针)。这意味着两个节点之间始终存在循环关系。可以通过为每个根节点(文档?)使用所有者类来处理它,或者使用 shared_ptr/weak_ptr 的混合方式,或者使用垃圾回收(GC),这意味着您可以持有 DOM 的部分或全部而不必真正关心哪一部分需要被销毁,只需设置某些指针为空即可... - paercebal
2
@Thomas Eding:... GC所带来的问题是,当你拿着一个微小的DOM节点时,却没有意识到整个树都依附在它上面。这是一种泄漏。而且,如果由于某些原因您的代码的某个隐藏部分持有对该微小节点的指针(例如某个事件侦听器,就像C#中的委托或Java中的匿名内部类),那么无论GC如何,您都会遇到泄漏的问题……总之,所有类型的内存处理都有各自的问题……在C++中拥有GC的好处是可以选择内存处理方式…… :-) - paercebal
显示剩余17条评论

12
简短回答就是垃圾收集原则上与 RAII 及智能指针类似。如果你所分配的每一块内存都在一个对象之内,并且该对象只被智能指针引用,那么你就有了接近垃圾收集(甚至更好)的东西。这样做的优势在于不必过于谨慎地处理作用域和为每个对象使用智能指针,而是让运行时为你完成这些工作。
这个问题看起来很像“C++ 对有经验的汇编开发人员有什么提供?指令和子程序消除了它的需求,对吗?”

3
如果您正在使用引用计数智能指针,请注意避免出现引用环。垃圾回收的一个优点是它不会受到引用环的困扰。 - CesarB
3
如果适当使用boost::weak_ptr,引用循环就不是问题。 - Head Geek
3
除了智能指针会在其超出作用域时释放资源之外,垃圾回收器可能会让它们存在很长时间。这可能会产生很大的影响。 - gbjbaanb
1
@Head Geek - 如果您正确使用汇编语言等技术(请参见Matt J答案的最后一段),那就太好了。 - Daniel Earwicker
@gbjbaanb: "the moment they go out of scope". 相反,GC可以在值超出范围之前收集它们,而引用计数智能指针可以使值保持活动状态直到其范围结束。 - J D
显示剩余2条评论

9
我不明白如何争论RAII是否替代垃圾回收或者远远优于它。许多情况下,gc可以处理的RAII根本无法处理。它们是不同的东西。
首先,RAII并非绝对可靠:它可以抵御一些在C++中普遍存在的问题,但是有许多情况下RAII毫无用处;它对于异步事件(例如UNIX下的信号)来说非常脆弱。从根本上说,RAII依赖于作用域:当一个变量超出作用域时,它就会被自动释放(当然要假定析构函数被正确实现了)。
这里有一个简单的例子,在这种情况下,无论是auto_ptr还是RAII都无能为力:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <memory>

using namespace std;

volatile sig_atomic_t got_sigint = 0;

class A {
        public:
                A() { printf("ctor\n"); };
                ~A() { printf("dtor\n"); };
};

void catch_sigint (int sig)
{
        got_sigint = 1;
}

/* Emulate expensive computation */
void do_something()
{
        sleep(3);
}

void handle_sigint()
{
        printf("Caught SIGINT\n");
        exit(EXIT_FAILURE);
}

int main (void)
{
        A a;
        auto_ptr<A> aa(new A);

        signal(SIGINT, catch_sigint);

        while (1) {
                if (got_sigint == 0) {
                        do_something();
                } else {
                        handle_sigint();
                        return -1;
                }
        }
}

A的析构函数永远不会被调用。当然,这是一个人为而有些牵强的例子,但实际上类似的情况确实会发生;例如,当您的代码被另一个您完全无法控制的处理SIGINT的代码调用时(具体示例:matlab中的mex扩展)。这也是为什么Python中的finally不能保证执行某些内容的原因之一。在这种情况下,gc可以帮助您。
其他习惯用法与此不同:在任何非平凡程序中,您都需要有状态的对象(我在这里使用对象一词,它可以是语言允许的任何结构);如果您需要在一个函数之外控制状态,则无法轻松使用RAII(这就是为什么RAII对于异步编程并不那么有用的原因)。另一方面,gc对您的进程的整个内存有一个视图,即它知道它分配的所有对象,并且可以异步清理。
使用垃圾回收器也可以更快,原因相同:如果需要分配/释放许多对象(特别是小对象),垃圾回收器将远远优于RAII,除非编写自定义分配器,因为垃圾回收器可以在一次传递中分配/清理许多对象。一些知名的C++项目即使在性能方面也使用了垃圾回收器(例如Tim Sweenie在Unreal Tournament中使用gc的文章:http://lambda-the-ultimate.org/node/1277)。垃圾回收基本上是以吞吐量换取延迟。

当然,有些情况下RAII比垃圾回收更好;特别是,垃圾回收概念主要涉及内存,而这并不是唯一的资源。像文件等东西可以很好地使用RAII进行处理。没有内存处理的语言(如Python或Ruby)在这些情况下也有类似于RAII的东西,BTW(Python中的with语句)。当您需要精确控制资源何时被释放时,RAII非常有用,对于文件或锁定等情况经常会出现这种情况。

3
更贴近事实的说法是,RAII能够处理许多垃圾回收无法处理的情况。垃圾回收主要处理内存,而RAII则可以处理任何类型的资源。据我所知,智能指针可以消除“脆弱性”论点。 - Head Geek
7
抱歉,但是这个例子似乎很虚假。在信号处理程序中调用exit()也无法让垃圾回收清理任何东西。 - Head Geek
2
RAII可以帮助线程锁,我同意,但它并非万能药。我认为我真正关心的是RAII是一个神奇的解决方案的想法,可以神奇地防止死锁、内存泄漏等问题...它绝对有用,但它不是一根魔棒。 - David Cournapeau
2
RAII 无疑存在其缺陷。但我并不认为 GC 能够解决这些问题,它只是将一组缺陷换成了另一组。 - Head Geek
2
嗯,你的例子即使使用GC也同样糟糕,因为即使对象在退出时被清理了,它的终结器仍然不会被调用(因为终结线程在第二次运行时才收集)。 - gbjbaanb
显示剩余9条评论

9
随着像valgrind这样的好的内存检查器的出现,我不认为垃圾回收作为安全保障“以防万一”我们忘记释放某些东西有多大用处——特别是因为它在管理除内存之外的更通用资源方面帮助不大(尽管这些资源较少)。此外,在我看来,即使使用智能指针,显式分配和释放内存也相当罕见,因为容器通常是更简单和更好的方式。

但是垃圾回收可能会提供性能优势,特别是如果有很多短暂的对象被堆分配。垃圾回收还可能为新创建的对象提供更好的引用局部性(与堆栈上的对象相当)。


Greg,请问您可以详细说明一下最后一段吗?我曾以为这是任何内存分配器的工作——包括malloc——而不仅仅是垃圾收集器(它们本质上是为您确定何时调用free())。但我在这方面不是专业人士,希望能得到更详细的解释。 - SquareCog
一个 gc 的潜在性能优势是你可以一次性进行分配/释放,而不是多次操作。这取决于具体情况:在某些环境中,手动内存分配或使用定制分配器的 RAII 可能比 gc 更容易处理。 - David Cournapeau

8

支持C++中GC的动机似乎是lambda编程、匿名函数等。事实证明,lambda库受益于能够分配内存而不必担心清理的能力。对普通开发人员的好处是更简单、更可靠和更快速地编译lambda库。

GC还有助于模拟无限内存;您需要删除POD的唯一原因是需要回收内存。如果您拥有GC或无限内存,则不再需要删除POD。


2
换句话说,这只是为经验不足的程序员提供的支持? :-) - Head Geek
8
只有当你认为函数式编程是给经验不足的程序员用的东西时,它才是如此。垃圾回收(gc)是一种非常强大的工具,但代价也很高:像所有强大的抽象一样,它使我们能够将注意力集中在手头的问题上,但有时会出现问题,并且需要深入了解其底层抽象。 - David Cournapeau
2
迄今为止我看到的第一个正确答案,给你点赞。你要找的技术术语是“向上funarg问题”,可以追溯到将近半个世纪前。http://dl.acm.org/citation.cfm?id=1093411 - J D

7
委员会并非添加垃圾回收,而是添加了一些功能,使得垃圾回收可以更安全地实现。只有时间才能告诉我们它们是否真正对未来的编译器产生任何影响。具体实现可能会有很大的差异,但最可能涉及基于可达性的收集,这可能会导致轻微的停顿,具体取决于如何执行。
不过,有一点需要注意,符合标准的垃圾回收器将无法调用析构函数 - 只能默默地重用丢失的内存。

你说得对,他们并没有“添加垃圾回收”。我看错了文章。 - Head Geek
“Couple of features”是正确的,C++不会依赖它,除非被使用(零成本抽象)。Stroustrup说链接自维基百科页面):“我的观点可以概括为‘C++是一种很好的垃圾收集语言,因为它创建的需要收集的垃圾非常少’。” - maxpolk

7

垃圾回收对于经验丰富的C++开发者有哪些优势?

不需要追踪你那些经验不足的同事代码中的资源泄漏问题。


3
如果你坚持让每个人都使用RAII和智能指针,资源泄漏就不可能发生。 - Head Geek
4
但是,制定和执行这些规则是有成本的,而只是将它们作为指导方针,并不意味着它们总是会被遵循。 - JohnMcG
4
使用RAII和智能指针时很容易发生内存泄漏。例如,信号处理程序改变了代码路径并永远不会返回给调用者:在这种情况下,无论是RAII还是智能指针都无法帮助您避免内存泄漏。 - David Cournapeau
资源获取即初始化:http://zh.wikipedia.org/wiki/资源获取即初始化 - David Cournapeau
2
就算是像.NET这样的智能标记清除垃圾收集器,也无法防止所有资源泄漏。 - FlySwat
我并不认为垃圾回收有助于资源管理。它最大的帮助之处在于允许使用不可变对象引用作为其内容的代理,而无需将内容视为资源。 - supercat

6
垃圾回收允许推迟关于谁拥有对象的决定。
C++使用值语义,因此通过RAII,对象在超出范围时确实被重新收集。这有时称为“立即垃圾回收”。
当您的程序开始使用引用语义(通过智能指针等),语言不再支持您,您只能依靠智能指针库的机智。
垃圾回收的棘手之处在于决定何时不再需要一个对象。

2
智能指针完全消除了决定谁拥有一个对象的需要。 - Head Geek
7
@Head Geek: 不完全正确。如果你有两个对象A和B,通过智能指针指向它们,你是正确的。现在,如果A也指向B,而B也指向A,那么你就有一个问题,必须通过使用weak_ptr和/或shared_ptr来决定谁拥有该对象。 - paercebal
你正在延续一个错误的观念,即对象在超出作用域后就变得无法访问。 - J D
@JonHarrop:在C++中,使用值语义时,当超出作用域时它们是不可访问的。我应该提到这一点。 - xtofl
这是正确的,但反过来(如果它们在范围内,则它们是可达的)通常不成立。 - J D

6

很多人错误地认为,因为C++语言本身没有内建垃圾回收机制,就不能在C++中使用垃圾回收。这是不正确的。我知道有精英级别的C++程序员会在他们的工作中使用Boehm垃圾回收器。


是的,我看过几个附加的垃圾收集库。但在大多数情况下,我不认为它们是必要的或值得拥有的。这个问题的答案给了我一些(非常少的)情况下需要它的原因,这也是我提出这个问题的原因。 - Head Geek

5

更易于线程安全和可扩展性

在某些情况下,GC的一个属性可能非常重要。在大多数平台上,指针的赋值自然是原子性的,而创建线程安全的引用计数(“智能”)指针则相当困难,并且会引入显着的同步开销。因此,智能指针通常被告知在多核架构上“不具有良好的可扩展性”。


1
那是一个有道理的观点,虽然我通常不会担心这个问题。当我进行多线程编程时,线程很少共享它们的数据结构。 - Head Geek
1
从技术上讲,在无限堆大小的渐近情况下,引用计数比其他垃圾回收算法更具可扩展性。 - J D

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