代码之家  ›  专栏  ›  技术社区  ›  Head Geek

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

  •  45
  • Head Geek  · 技术社区  · 17 年前

    我一直听到人们抱怨C++没有垃圾收集。我还听说C++标准委员会正在考虑将它添加到语言中。恐怕我不明白其中的意义…使用带有智能指针的RAII就不需要它了,对吗?

    我唯一的垃圾收集经验是在几个便宜的80年代家庭电脑,这意味着系统会冻结几秒钟,每一次如此频繁。我相信从那以后情况有所改善,但你可以猜到,这并没有让我对它产生很高的评价。

    垃圾回收能给经验丰富的C++开发人员带来什么好处?

    16 回复  |  直到 11 年前
        1
  •  67
  •   Community Mohan Dere    9 年前

    我一直听到人们抱怨C++没有垃圾收集。

    我为他们感到抱歉。说真的。

    C++有RAII,我总是抱怨在垃圾收集语言中找不到RAII(或者阉割RAII)。

    垃圾回收能给经验丰富的C++开发人员带来什么好处?

    另一个工具。

    马特J写得很对( Garbage Collection in C++ -- why? 我们不需要C++的特性,因为它们中的大多数都可以用C语言编码,我们不需要C特性,因为大多数都可以在汇编等中编码。 C++必须进化。

    作为开发人员:我不在乎GC。我尝试了RAII和GC,我发现RAII非常优越。正如格雷格·罗杰斯在他的职位上所说( Garbage Collection in C++ -- why? 内存泄漏并不是那么可怕(至少在C++中,如果C++被使用的话,它们是罕见的)来证明GC而不是RAII。GC具有不确定性的释放/终结,只是一种 编写一个不关心特定内存选择的代码 .

    最后一句话很重要:写“juste不在乎”的代码很重要。以同样的方式,在C++中,我们不关心释放源,因为RAII为我们做了,或者对象初始化,因为构造函数为我们做了,有时只需编码而不关心谁是什么内存的所有者,以及我们需要什么样的指针(共享、弱等)。 在C++中似乎需要GC。 (即使我个人看不见)

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

    有时候,在应用程序中,你有“浮动数据”。想象一个树形的数据结构,但没有人真正是数据的“所有者”(也没有人真正关心数据何时会被销毁)。多个对象可以使用它,然后丢弃它。你希望在没有人再使用它的时候释放它。

    C++方法是使用智能指针。我想到了共享资源。所以每个数据都由自己的共享指针拥有。酷。问题是,当每一段数据都可以引用另一段数据时。不能使用共享指针,因为它们使用的引用计数器不支持循环引用(A指向B,B指向A)。因此,您必须了解在哪里使用弱指针(boost::weak_ptr)以及何时使用共享指针。

    对于GC,您只需使用树结构数据。

    缺点是你不必在意 什么时候? “浮动数据”将真正被销毁。只有它 摧毁。

    结论

    最后,如果做得正确,并且与C++的当前习语兼容,GC将是一个 C++的另一个好工具 .

    C++是一种多范式语言:添加GC可能会使一些C++迷因为叛逆而哭泣,但最终,它可能是一个好主意,我猜想C++标准COMITEE不会让这种主要特征破坏语言,所以我们可以相信他们做出必要的工作来启用一个不干扰C++的正确的C++ GC: 像C++一样,如果你不需要一个特性,不要使用它,它不会花费你任何东西。

        2
  •  11
  •   Matt J Jørgen Fogh    17 年前

    简而言之,垃圾收集在原则上与带有智能指针的RAII非常相似。如果您分配的每一块内存都位于一个对象中,并且该对象只被智能指针引用,那么您就拥有了一些接近垃圾收集的东西(可能更好)。其优点在于不必如此明智地确定范围和智能地为每个对象指定指针,并且让运行时为您完成工作。

    这个问题类似于“C++如何为有经验的装配开发者提供什么?”指令和子程序消除了对它的需求,对吗?”

        3
  •  9
  •   Greg Rogers    17 年前

    随着像valgrind这样优秀的内存检查程序的出现,我认为垃圾收集作为一个安全网“以防万一”并没有多大用处,我们忘记了释放一些东西——特别是因为它对管理除内存以外的更通用的资源情况没有多大帮助(尽管这些情况不太常见)。此外,在我看到的代码中,显式地分配和释放内存(即使使用智能指针)是相当罕见的,因为容器通常是一种更简单和更好的方法。

    但是垃圾收集可以潜在地提供性能优势,特别是在大量短期对象被堆分配的情况下。GC还可能为新创建的对象(与堆栈上的对象相比)提供更好的引用位置。

        4
  •  8
  •   MSalters    17 年前

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

    GC还帮助模拟无限内存;您需要删除pods的唯一原因是需要回收内存。如果您有GC或无限内存,就不再需要删除pods了。

        5
  •  7
  •   coppro    17 年前

    委员会没有添加垃圾收集,而是添加了一些功能,使垃圾收集更安全地实现。只有时间才能判断它们是否对未来的编译器有任何影响。具体的实现可能会有很大的差异,但最有可能涉及基于可到达性的集合,这可能涉及一个轻微的挂起,具体取决于它是如何完成的。

    不过,有一件事是,没有符合标准的垃圾收集器能够调用析构函数——只能默默地重用丢失的内存。

        6
  •  7
  •   JohnMcG    17 年前

    垃圾回收能给经验丰富的C++开发人员带来什么好处?

    不必追查经验不足的同事代码中的资源泄漏。

        7
  •  7
  •   David Cournapeau    17 年前

    我不明白人们怎么能说RAII取代了GC,或者说它是非常优越的。有许多由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;
                    }
            }
    }
    

    永远不会调用的析构函数。当然,这是一个人为的,有点做作的例子,但实际上也可能发生类似的情况;例如,当您的代码被另一个处理sigint的代码调用时,您完全无法控制它(具体的例子:matlab中的mex扩展)。这也是为什么在Python中finally不能保证某个东西的执行的原因。在这种情况下,GC可以帮助您。

    其他的习语不能很好地处理这个问题:在任何一个重要的程序中,你都需要有状态的对象(我在这里用的是非常广义的“对象”一词,它可以是语言所允许的任何构造);如果你需要在一个函数之外控制状态,你就不能用raii轻松地做到这一点(这就是为什么raii对异步HR没有帮助的原因)。Onous编程)。gc有一个进程的整个内存视图,也就是它知道它分配的所有对象,并且可以异步清理。

    出于同样的原因,使用GC也会更快:如果需要分配/取消分配多个对象(尤其是小对象),除非编写自定义分配器,否则GC的性能将大大优于RAII,因为GC可以一次分配/清除多个对象。一些著名的C++项目使用GC,即使在性能方面(例如,Tim Sweenie在非真实竞赛中使用GC): http://lambda-the-ultimate.org/node/1277 )GC基本上以延迟为代价增加了吞吐量。

    当然,有些情况下raii比gc更好;特别是,gc的概念主要与内存有关,这不是唯一的资源。像文件之类的东西…可以很好地处理RAII。没有内存处理的语言(如python或ruby)在这些情况下确实具有类似raii的特性,btw(在python中使用语句)。当您精确地需要控制何时释放资源时,raii非常有用,例如,对于文件或锁,这种情况非常常见。

        8
  •  6
  •   tragomaskhalos    17 年前

    假设C++没有垃圾收集,这是一个普遍的错误。 融入语言 不能在C++期间使用垃圾回收。这是胡说八道。我知道那些使用BEHM收藏家的精英C++程序员在他们的工作中是理所当然的。

        9
  •  6
  •   xtofl Adam Rosenfield    13 年前

    垃圾收集允许 推迟 关于谁的决定 拥有 物体。

    C++使用值语义,所以RAI实际上是在离开范围时回忆对象。这有时被称为“即时GC”。

    当程序开始使用引用语义(通过智能指针等)时,该语言不再支持您,您只能使用智能指针库。

    GC的棘手之处在于 什么时候? 不再需要对象。

        10
  •  5
  •   Ben Voigt    14 年前

    垃圾收集使 RCU 无锁同步更容易正确有效地实现。

        11
  •  5
  •   Suma    14 年前

    更简单的线程安全性和可扩展性

    GC的一个属性在某些情况下可能非常重要。在大多数平台上,指针的分配自然是原子的,而创建线程安全引用计数(“smart”)指针则相当困难,并且会带来大量的同步开销。因此,在多核架构中,智能指针经常被告知“不能很好地伸缩”。

        12
  •  3
  •   Luke Quinane George    17 年前

    垃圾收集实际上是自动资源管理的基础。让GC以一种难以量化的方式改变您处理问题的方式。例如,在进行手动资源管理时,需要:

    • 考虑何时可以释放一个项(所有模块/类都完成了吗?)
    • 考虑当一个资源准备释放时,谁负责释放它(哪个类/模块应该释放这个项目?)

    在一般情况下,不存在复杂性。例如,在方法开始时打开一个文件,在结束时关闭它。或者调用方必须释放返回的内存块。

    当您有多个模块与一个资源交互时,事情会很快变得复杂起来,而且还不清楚需要清理哪些模块。最终的结果是,解决问题的整个方法包括一些妥协的编程和设计模式。

    在具有垃圾收集功能的语言中,可以使用 disposable 模式,您可以释放您知道已经完成的资源,但如果您不能释放它们,GC将在那里保存一天。


    智能指针实际上是我提到的妥协的完美例子。除非有备份机制,否则智能指针无法避免泄漏循环数据结构。为了避免这个问题,您经常妥协并避免使用循环结构,即使它在其他方面可能是最合适的。

        13
  •  2
  •   ADEpt    17 年前

    我也怀疑C++成员正在向标准中添加一个完整的垃圾收集。

    但我想说,用现代语言添加/进行垃圾收集的主要原因是,好的理由太少了 反对 垃圾收集。自80年代以来,在内存管理和垃圾收集领域有了一些巨大的进步,我相信甚至有垃圾收集策略可以为您提供软实时的保证(例如,“GC不会花费超过….在最坏的情况下)。

        14
  •  2
  •   J D    13 年前

    使用带有智能指针的RAII就不需要它了,对吗?

    智能指针可以用来实现C++中的引用计数,这是一种垃圾收集的形式(自动内存管理),但是生产GCS不再使用引用计数,因为它有一些重要的缺陷:

    1. 参考计数泄漏周期。考虑到a b,两个对象a和b都互相引用,因此它们都有一个引用计数1,两者都不被收集,但它们都应该被回收。高级算法如 trial deletion 解决这个问题,但增加了很多复杂性。使用 weak_ptr 因为一个解决方法正在回归到手动内存管理。

    2. 天真的参考计数缓慢有几个原因。首先,它要求经常缓冲缓存外引用计数(请参见 Boost's shared_ptr up to 10× slower than OCaml's garbage collection )。其次,在作用域末尾注入的析构函数可能会导致不必要的、昂贵的虚拟函数调用,并抑制诸如尾调用消除之类的优化。

    3. 基于作用域的引用计数保持了浮动垃圾的存在,因为对象直到作用域结束时才被回收,而跟踪GCS可以在对象不可访问时立即回收它们,例如,在循环期间回收循环之前是否可以分配本地?

    垃圾回收能给经验丰富的C++开发人员带来什么好处?

    生产力和可靠性是主要优势。对于许多应用程序,手工内存管理需要大量的程序员工作。通过模拟无限内存机,垃圾收集将程序员从这个负担中解放出来,使他们能够集中精力解决问题,并避免一些重要的bug类(悬空指针、丢失 free 自由的 )此外,垃圾收集有助于其他形式的编程,例如通过解决 upwards funarg problem (1970) .

        15
  •  2
  •   supercat    11 年前

    在支持GC的框架中,对不可变对象(如字符串)的引用可以以与基元相同的方式传递。考虑类(C或Java):

    public class MaximumItemFinder
    {
      String maxItemName = "";
      int maxItemValue = -2147483647 - 1;
    
      public void AddAnother(int itemValue, String itemName)
      {
        if (itemValue >= maxItemValue)
        {
          maxItemValue = itemValue;
          maxItemName = itemName;
        }
      }
      public String getMaxItemName() { return maxItemName; }
      public int getMaxItemValue() { return maxItemValue; }
    }
    

    请注意,此代码与 内容 任何一个字符串,并且可以简单地将它们视为基元。像这样的陈述 maxItemName = itemName; 可能会生成两条指令:寄存器加载,然后是寄存器存储。这个 MaximumItemFinder 将无法知道 AddAnother 将保留对传入字符串的任何引用,调用方将无法知道多长时间 最大温度探测器 将保留对它们的引用。呼叫者 getMaxItemName 不知道是何时何地 最大温度探测器 而返回字符串的原始供应商已经放弃了对它的所有引用。但是,因为代码可以像原语值一样简单地传递字符串引用, 这些都不重要 .

    还要注意的是,当同时调用 添加另一个 , 任何调用 GetMaxItemName 将保证返回对空字符串或已传递到的字符串之一的有效引用 添加另一个 . 如果要确保最大项名称与其值之间的任何关系,则需要线程同步,但是 即使没有记忆安全也能保证 .

    我认为没有任何方法可以像C++那样编写一种方法,它可以在任意多线程使用的情况下支持内存安全,而不需要使用线程同步,或者要求每个字符串变量都有自己的内容拷贝,保存在其自己的存储空间中,而这些变量可能不会被释放或重新定位。对相关变量的生存期进行循环。当然不可能定义一个字符串引用类型,它可以像 int .

        16
  •  1
  •   Dragon Energy    11 年前

    垃圾收集会让泄漏成为你最可怕的噩梦。

    处理循环引用之类的完全成熟的GC在一定程度上是对已计数的引用的升级。 shared_ptr . 我对C++有点欢迎,但在语言层面上不太欢迎。

    关于C++的一个优点是它不会强制垃圾收集。

    我想纠正一个常见的误解:垃圾收集 神话 以某种方式消除泄漏。根据我的经验,调试他人编写的代码并试图发现最昂贵的逻辑泄漏的最可怕的噩梦是通过资源密集型主机应用程序使用诸如嵌入式Python之类的语言进行垃圾收集。

    当谈论像GC这样的主题时,先有理论,然后有实践。从理论上讲,它很好,可以防止泄漏。然而,在理论层面上,每种语言都是美妙的,并且没有漏洞,因为从理论上讲,每个人都会写出完全正确的代码,并测试单个代码可能出错的每一个可能的情况。

    垃圾收集加上不太理想的团队协作,导致了我们案例中最糟糕、最难调试的泄漏。

    这个问题仍然与资源的所有权有关。当涉及持久性对象时,您必须在这里做出明确的设计决策,垃圾收集使您很容易认为自己没有。

    有了一些资源, R 在团队环境中,在开发人员不经常交流和审查彼此的代码的情况下(在我的经验中有点太常见),开发人员很容易理解 A 存储该资源的句柄。开发商 B 也一样,可能以一种模糊的方式间接地增加 R 一些数据结构。所以 C . 在垃圾收集系统中,这已经创建了 R .

    因为开发者 是最初创建资源并认为自己是该资源的所有者的资源,他记得要发布对 R 当用户表示不再想使用它时。毕竟,如果他不这样做,就不会发生任何事情,而且从测试中可以明显看出,用户端删除逻辑没有做任何事情。所以他记得像任何一个有能力的开发者一样发布它。这会触发一个事件, 处理它并记住释放对 R .

    然而, C 忘记。他并不是团队中实力较强的开发人员之一:一个刚刚加入系统工作一年的新人。或者他甚至不在团队中,只是一个受欢迎的第三方开发人员为我们的产品编写插件,许多用户将其添加到软件中。对于垃圾收集,这是我们得到这些静默逻辑资源泄漏的时候。它们是最糟糕的类型:它们不一定在软件的用户可见端作为一个明显的bug表现出来,除了运行程序的时间过长之外,内存的使用只是为了某种神秘的目的而不断增加和增加。尝试用调试器缩小这些问题的范围,可能和调试时间敏感的竞态条件一样有趣。

    没有垃圾收集,开发人员 C 会创建一个 悬空指针 . 他可能会试图在某个时候访问它,导致软件崩溃。现在这是一个测试/用户可见的bug。 C 有点尴尬,纠正了他的错误。在GC场景中,仅仅试图找出系统泄漏的位置可能非常困难,以至于某些泄漏永远不会得到纠正。这些不是 valgrind -键入可以轻松检测并精确定位到特定代码行的物理泄漏。

    有了垃圾收集,开发者 C 造成了一个非常神秘的泄露。他的代码可能会继续访问 R 它现在只是软件中一些不可见的实体,此时与用户无关,但仍处于有效状态。作为 C 他的代码产生了更多的泄漏,他在不相关的资源上创建了更多的隐藏处理,软件不仅泄漏内存,而且每次都变得越来越慢。

    因此,垃圾收集不一定能够缓解逻辑资源泄漏。在不太理想的情况下,它可以使泄漏更容易被忽略并保留在软件中。开发人员在试图跟踪GC逻辑泄漏时可能会非常沮丧,他们只是告诉用户定期重启软件作为解决方法。它确实消除了悬空指针,在一个安全软件中,崩溃在任何情况下都是完全不可接受的,那么我更喜欢GC。但我经常在安全性不高但资源密集型、性能关键型产品中工作,在这些产品中,可以迅速修复的崩溃比一个非常隐蔽和神秘的无声错误更可取,而且资源泄漏在这些产品中并不是微不足道的错误。

    在这两种情况下,我们谈论的都是不在堆栈上的持久对象,比如3D软件中的场景图,或者排字器中可用的视频剪辑,或者游戏世界中的敌人。当资源将其生命周期绑定到堆栈时,C++和任何其他GC语言都倾向于使管理资源正常。真正的困难在于引用其他资源的持久性资源。

    在C或C++中,如果你不能清楚地指定谁拥有一个资源,并且当对它们的处理应该被释放(EX:响应于事件设置为NULL),则可以有由分段错误导致的悬空指针和崩溃。然而,在GC中,这种响亮、令人讨厌但往往容易发现的崩溃被交换为可能永远检测不到的无声资源泄漏。