【问题标题】:How to guard against memory leaks?如何防范内存泄漏?
【发布时间】:2011-01-12 05:41:21
【问题描述】:

我最近在面试 C++ 职位,有人问我如何防止造成内存泄漏。我知道我没有对这个问题给出满意的答案,所以我把它扔给你们。防止内存泄漏的最佳方法是什么?

谢谢!

【问题讨论】:

  • 使用垃圾收集器 (google.com/search?q=Garbage+collection+c%2B%2B)...?
  • @KennyTM 不,当您拥有 RAII 时,不要使用垃圾收集器。如果您确实需要共享所有权,只需使用 C++0x 中的 shared_ptr 或目前的 boost。
  • @Kenny:如果你想忍受与 GC 相关的问题。 C++ 有一个更细粒度的控制机制,称为智能指针。
  • 感谢大家的详尽解答。决定接受哪个答案很困难。由于 Poita_ 的回答有很多赞成票,我会让那个人自己说话。我已将 jalf 的回答标记为答案,因为它对他的观点有学术考虑。再次感谢!
  • @KennyTM:C++ 中的 GC 并不能解决问题。 C++ 中的 GC 必须对正在使用的内存做出非常保守的最坏情况估计,这意味着它可能会错过一些分配。所以在 C++ 中,GC 不会消除内存泄漏,它只是处理其中的一些

标签: c++ memory-management pointers memory-leaks


【解决方案1】:

您可以使用该实用程序。 如果你在 Linux 上工作 - 使用 valgrid(它是免费的)。 在 Windows 上使用 deleaker。

【讨论】:

    【解决方案2】:

    为避免内存泄漏,您必须对谁负责删除任何动态分配的对象有一个清晰明确的概念。

    C++ 允许在堆栈上构造对象(即作为一种局部变量)。这将控制流的创建和销毁绑定在一起:当程序执行到达其声明时创建一个对象,并且当执行转义进行该声明的块时该对象被销毁。每当分配需要与该模式匹配时,就使用它。这样可以省去很多麻烦。

    对于其他用法,如果您可以定义并记录明确的责任概念,那么这可能会很好。例如,您有一个方法或函数返回一个指向新分配对象的指针,并且您记录了调用者将负责最终删除该实例。清晰的文档加上良好的程序员纪律(这是不容易实现的!)可以解决内存管理的许多剩余问题。

    在某些情况下,包括散漫的程序员和复杂的数据结构,您可能不得不求助于更高级的技术,例如引用计数。每个对象都被授予一个“计数器”,它是指向它的其他变量的数量。每当一段代码决定不再指向该对象时,计数器就会减少。当计数器达到零时,对象被删除。引用计数需要严格的计数器处理。这可以通过所谓的“智能指针”来完成:这些对象在功能上是指针,但会在自己的创建和销毁时自动调整计数器。

    引用计数在许多情况下都非常有效,但它们无法处理循环结构。所以对于最复杂的情​​况,你必须求助于重型火炮,即garbage collector。我链接到的是 Hans Boehm 编写的 C 和 C++ 的 GC,它已在一些相当大的项目中使用(例如 Inkscape)。垃圾收集器的目的是维护整个内存空间的全局视图,以了解给定实例是否仍在使用中。当本地视图工具(例如引用计数)不够用时,这是正确的工具。有人可能会争辩说,在这一点上,人们应该问自己 C++ 是否是解决手头问题的正确语言。当语言是协作的时,垃圾收集效果最好(这解锁了许多优化,当编译器不知道内存发生了什么时,这些优化是不可行的,作为典型的 C 或 C++ 编译器)。

    请注意,上述任何技术都不能让程序员停止思考。甚至 GC 也会遭受内存泄漏的影响,因为它使用 reachability 作为 future 使用 的近似值(有理论上的原因表明它是不可能的,一般来说,以准确检测所有此后不会使用的对象)。您可能仍需要将某些字段设置为 NULL 以通知 GC 您将不再通过给定变量访问对象。

    【讨论】:

      【解决方案3】:

      到目前为止给出的所有答案都归结为:避免致电delete

      任何时候程序员必须调用delete,你都有潜在的内存泄漏。 相反,让delete 调用自动发生。 C++ 保证本地对象在超出范围时调用其析构函数。使用该保证来确保您的内存分配被自动删除。

      在最普遍的情况下,这种技术意味着每个内存分配都应该包装在一个简单的类中,其构造函数分配必要的内存,而析构函数释放它。

      由于这是一种常用且应用广泛的技术,因此创建了智能指针类来减少样板代码的数量。它们的构造函数不是分配内存,而是获取一个指向已经分配的内存的指针,并将其存储起来。当智能指针超出范围时,它能够删除分配。

      当然,根据使用情况,可能需要不同的语义。您是否只需要简单的情况,即分配应该持续到包装类存在的时间?然后使用boost::scoped_ptr,或者,如果你不能使用boost,则使用std::auto_ptr。您是否有未知数量的对象引用分配,但不知道每个对象将存活多久?那么引用计数的boost::shared_ptr 是一个很好的解决方案。

      但您不必使用智能指针。标准库容器也可以做到这一点。它们在内部分配存储您放入其中的对象副本所需的内存,并在删除它们时再次释放内存。因此用户不必调用newdelete

      这种技术有无数种变体,改变了创建初始内存分配的责任,或者何时应该执行释放。

      但它们的共同点是您的问题的答案:RAII 习语:资源获取即初始化。内存分配是一种资源。资源应该在对象初始化时获取,在对象销毁时由对象自身释放。

      让 C++ 范围和生命周期规则为您工作。永远不要在 RAII 对象之外调用delete,无论它是容器类、智能指针还是单个分配的特定包装器。让对象处理分配给它的资源。

      如果所有delete 呼叫都是自动发生的,那么您将无法忘记它们。然后就没有办法泄漏内存了。

      【讨论】:

      • 请注意,这可能会导致与毫无幽默感的面试官进行尴尬的面试:“你如何避免内存泄漏?” ...“避免写删除!”...
      • 更有理由说出来 ;)
      • -1 是的,仍然有一些方法可以导致内存泄漏;循环依赖。
      • @Viktor:是的,不正确的代码仍然是不正确的。或许您可以向我展示一种解决那个问题的技术?
      • @Viktor:您假设 RAII== 引用计数。事实并非如此。循环依赖如何防止我的scoped_ptr 删除它指向的对象。引用计数(和shared_ptr)是一个 RAII 的特例,它有一些关键的弱点,而且被过度使用。但这并没有改变我所说的,RAII 通常是避免泄漏的方法。
      【解决方案4】:

      除了关于 RAII 的建议之外,如果有任何虚函数,请记住将基类析构函数设为虚函数。

      【讨论】:

        【解决方案5】:
        • 确保您准确了解每次创建对象时将如何删除对象
        • 确保每次将指针返回给您时,您都了解谁拥有指针
        • 确保您的错误路径正确处理您创建的对象
        • 对以上内容偏执

        【讨论】:

          【解决方案6】:
          1. (Easy) 永远不要让原始指针拥有对象(在代码中搜索正则表达式 "\= *new"。使用 shared_ptrscoped_ptr 相反,甚至更好的是,尽可能多地使用实变量而不是指针。

          2. (Hard) 确保您没有任何循环引用,且 shared_ptrs 相互指向,使用 weak_ptr 来打破它们。

          完成!

          【讨论】:

            【解决方案7】:

            你最好阅读RAII

            【讨论】:

              【解决方案8】:

              在 x86 上,您可以定期使用 Valgrind 来检查您的代码

              【讨论】:

                【解决方案9】:
                1. 如果不需要,不要在堆上分配内存。大多数工作都可以在堆栈上完成,因此您应该只在绝对需要时才进行堆内存分配。

                2. 如果您需要一个由单个其他对象拥有的堆分配对象,请使用std::auto_ptr

                3. 使用标准容器或来自 Boost 的容器,而不是自己发明。

                4. 如果您有一个对象被多个其他对象引用并且不属于任何人,那么请使用std::tr1::shared_ptrstd::tr1::weak_ptr - 以适合您的用例为准。

                5. 如果这些都不符合您的用例,那么可以使用delete。如果您最终不得不手动管理内存,那么只需使用内存泄漏检测工具来确保您没有泄漏任何东西(当然,要小心)。不过,你不应该真的走到这一步。

                【讨论】:

                • @Viktor:不,很多人在不知道自己在做什么的情况下编写自己的容器。一小部分 C++ 程序员能够正确编写容器这一事实并不意味着绝大多数人不会从被告知停止并使用标准容器中受益。
                • std::auto_ptr 已被弃用。请改用std::unique_ptr
                • @Spidey,那么您对 ​​C++ 的看法不同于该领域的杰出人物和领导者,他们明显站在 RAII 一边(因此堆栈分配或使用智能指针)。
                • @Spidey:您在代码中使用异常吗?通过手动配对 new 和 delete 来编写异常安全的代码几乎是不可能的。
                • @Adrian: unique_ptr 在当前的 C++ 标准中不存在。 @Spidey:没有程序员可以在一个不平凡的 C++ 程序中“管理自己的内存”,而不利用 RAII。当然,RAII 不仅仅是智能指针,这是一个经常被遗忘的点。如果您只是说智能指针不是圣杯,那么您是对的。但是“原始”删除调用在每个级别上都是错误的。理智的 C++ 程序员“管理他们的内存”的方式是确保对象在当它们应该释放时自动释放。这与使用shared_ptr 对所有内容进行引用计数并不完全相同
                【解决方案10】:
                • 智能指针。
                • 内存管理。
                • 覆盖“新建”和“删除”或使用您自己的宏/模板。

                【讨论】:

                  【解决方案11】:

                  一个很好的方法是使用智能指针,boost/tr1::shared_ptr。一旦(堆栈分配的)智能指针超出范围,内存将被释放。

                  【讨论】:

                    【解决方案12】:

                    【讨论】:

                      【解决方案13】:

                      用 shared_ptr 替换 new。基本上是RAII。使代码异常安全。尽可能使用 stl。如果您使用引用计数指针,请确保它们不会形成循环。来自 boost 的 SCOPED_EXIT 也非常有用。

                      【讨论】:

                      • 请说明如何用共享指针替换“新”。
                      • boost::shared_ptr ptr = boost::shared_ptr(new T());
                      • 使用新的 - 几乎没有替换任何东西。
                      • 我会说 boost::shared_ptr ptr(new T);会做同样的事情,但保存一个临时对象的创建和复制
                      【解决方案14】:

                      使用各种智能指针。

                      使用特定的策略来创建和删除对象,例如谁负责删除。

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 2010-12-20
                        • 2010-09-21
                        • 2019-10-17
                        • 2015-06-11
                        • 2011-08-09
                        • 2012-03-30
                        • 1970-01-01
                        相关资源
                        最近更新 更多