如何防止内存泄漏?

11

我最近参加了一次 C++ 职位的面试,被问到如何防止内存泄漏。我知道我的回答并不令人满意,所以我把这个问题留给你们了。有哪些最好的方法可以防止内存泄漏呢?

谢谢!


使用垃圾回收器(http://www.google.com/search?q=Garbage+collection+c%2B%2B)吗? - kennytm
2
@KennyTM 不要使用垃圾回收器,当你有RAII时。如果你真的需要共享所有权,只需使用C++0x或当前的boost中的shared_ptr即可。 - Khaled Alshaya
4
如果你想要忍受与垃圾回收相关的问题,C++有一种更好的细粒度控制机制,称为智能指针。 - Martin York
非常感谢大家提供的详尽答案。选择哪个答案来接受确实很困难。由于Poita_的回答得到了很多赞,我会让它自己说话。我已经将jalf的回复标记为答案,因为它涉及到了他的观点的学术考虑。再次感谢! - just_wes
@KennyTM:在C++中使用垃圾回收并不能解决问题。在C++中使用垃圾回收需要对内存的使用情况进行非常保守的最坏情况估计,这意味着它可能会错过一些内存分配。因此,在C++中,垃圾回收并不能消除内存泄漏问题,它只能处理其中的一部分。 - jalf
14个回答

21
所有已经给出的答案都可以概括为:避免调用delete。任何时候程序员必须调用delete,都可能导致内存泄漏。相反,让delete调用自动发生。C++保证本地对象在超出其范围时调用其析构函数。利用这种保证来确保您的内存分配被自动删除。最常见和广泛适用的技术是,将每个内存分配包装在一个简单的类中,其构造函数分配所需的内存,析构函数释放它。智能指针类已经创建,以减少样板代码的数量。它们的构造函数不是分配内存,而是采用对已经进行的内存分配的指针并存储该指针。当智能指针超出范围时,它能够删除分配。标准库容器也可以做到这一点。它们内部分配了存储放入其中对象的副本所需的内存,并在删除它们时释放内存。因此,用户无需调用newdelete。这种技术有无数变化形式,改变了谁来创建初始内存分配,或者何时执行解除分配。但它们共同回答了您的问题:RAII范式:资源获取即初始化。内存分配是一种资源。资源应在对象初始化时获取,并在对象本身被销毁时释放。

让C++的作用域和生命周期规则为您服务。永远不要在RAII对象之外调用delete,无论它是一个容器类、智能指针还是某个单独分配内存的临时包装器。让对象处理其所分配的资源。

如果所有的delete调用都是自动发生的,那么您就不可能忘记它们。这样就没有内存泄漏的风险了。


3
请注意,这可能会导致与幽默感缺失的面试官尴尬的面试环节:“你如何避免内存泄漏?”……“避免写delete!” - paercebal
2
更有理由说出来 ;) - jalf
1
-1 是的,仍然有可能出现内存泄漏;循环依赖。 - Viktor Sehr
1
@Viktor:是的,错误的代码仍然是错误的。也许你可以向我展示一种解决“那个”问题的技巧? - jalf
@Viktor:你认为RAII等于引用计数,这是不正确的。循环依赖如何防止我的scoped_ptr删除它所指向的对象。引用计数(和shared_ptr)是RAII的一个特例,并且它具有一些关键性缺陷,而且被过度使用了。但这并没有改变我所说的,即RAII 总体上是避免泄漏的方法。 - jalf

20
  1. 如果没有必要,不要在堆上分配内存。大多数工作都可以在栈上完成,因此只有在绝对需要时才应进行堆内存分配。

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

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

  4. 如果您有一个被多个其他对象引用且没有特定所有者的对象,则使用std :: tr1 :: shared_ptr std :: tr1 :: weak_ptr - 适合您的用例。

  5. 如果以上这些情况都不适用于您的用例,则可能使用delete。如果最终确实需要手动管理内存,请使用内存泄漏检测工具确保您没有泄漏任何内容(当然,一定要小心)。但您不应该真正走到这一步。


10
@Viktor: 不,很多人在不知道自己在做什么的情况下编写自己的容器。少数能够正确编写容器的C++程序员并不意味着绝大多数人不会受益于被告知停止使用自己编写的容器而改用标准容器。 - jalf
3
std::auto_ptr 已被弃用,请使用 std::unique_ptr - Adrian McCarthy
1
@Spidey,那么你对C++的看法与明显支持RAII(因此支持栈分配或使用智能指针)的该领域的权威和领袖们不同。 - Barry Wark
2
@Spidey:你在代码中使用异常吗?手动配对new和delete几乎不可能编写安全的异常处理代码。 - Nemanja Trifunovic
6
@Adrian:尽管如此,unique_ptr在当前的C++标准中并不存在。 @Spidey:在一个非平凡的C++程序中,没有程序员能够"管理自己的内存",除非利用RAII。当然,RAII远不止是智能指针,这一点经常被忽略。如果你仅仅是指智能指针不是万能的,那么你是正确的。但是,在任何情况下都不应该使用“裸”的delete调用。理智的C++程序员“管理他们的内存”的方法是确保对象在它们应该被释放的时候自动释放。这与使用shared_ptr使所有东西引用计数并不完全相同。 - jalf
显示剩余10条评论

8

你最好学习一下RAII(资源获取即初始化)的知识。点击这里了解更多。


7

使用shared_ptr替换new操作,基本上是RAII(资源获取即初始化)的应用。这样可以使代码具有异常安全性。尽可能地在代码中使用STL。如果您使用引用计数指针,请确保它们不会形成循环。来自boost的SCOPED_EXIT也非常有用。


1
请说明如何使用共享指针替换“new”。 - anon
1
boost::shared_ptr<T> ptr = boost::shared_ptr<T>(new T()); boost::shared_ptr<T> ptr = boost::shared_ptr<T>(new T()); - Chris H
1
这使用了新的东西 - 几乎没有替换任何东西。 - anon
我认为 boost::shared_ptr<T> ptr(new T); 可以做到同样的效果,但可以节省临时对象的创建和复制。 - davka

3
  1. (简单) 永远不要让一个裸指针拥有一个对象(在你的代码中搜索正则表达式"\= *new")。使用shared_ptrscoped_ptr代替,甚至更好的做法是尽可能使用真实变量而不是指针。

  2. (困难) 确保没有循环引用,即共享指针相互引用的情况,使用weak_ptr来打破它们。

完成!


2

使用各种智能指针。

采用特定的对象创建和删除策略,例如谁创建了对象就应该负责删除它。


2
  • 确保每次创建对象时,您都清楚地了解对象将如何被删除
  • 确保您明确了解每次返回指针时谁拥有该指针
  • 确保您的错误路径正确处理您创建的对象
  • 对以上内容要非常谨慎

2

除了RAII的建议外,如果有任何虚函数,请记得将基类析构函数声明为虚函数。


2
为了避免内存泄漏,你必须清楚明确地知道谁负责删除任何动态分配的对象。
C++ 允许在堆栈上构建对象(即一种本地变量)。这将创建和销毁绑定到控制流程:当程序执行到达其声明时,对象被创建,并且当执行逃脱该声明所在的块时,对象被销毁。每当分配需要匹配该模式时,请使用它。这将节省您很多麻烦。
对于其他用法,如果您可以定义并记录清晰的责任概念,则可能会很好。例如,您有一个方法或函数,该方法或函数返回指向新分配对象的指针,并且您记录调用者最终负责删除该实例。清晰的文档结合良好的程序员纪律(这不容易实现!)可以解决许多剩余的内存管理问题。
在某些情况下,如无纪律的程序员和复杂的数据结构,您可能需要采用更高级的技术,例如引用计数。每个对象都被授予一个“计数器”,即指向它的其他变量的数量。每当一段代码决定不再指向该对象时,计数器就会减少。当计数器达到零时,对象将被删除。引用计数需要严格的计数器处理。这可以通过所谓的“智能指针”来完成:这些对象在其自身创建和销毁时会自动调整计数器,它们在功能上是指针。
引用计数在许多情况下运作得很好,但无法处理循环结构。因此,在最复杂的情况下,您必须求助于重型武器,即垃圾回收器。我链接的是由Hans Boehm编写的C和C ++的GC,它已被用于一些相当大的项目(例如Inkscape)。垃圾回收器的目的是维护对完整内存空间的全局视图,以了解给定实例是否仍在使用中。当本地视图工具(如引用计数)不足时,这是正确的工具。有人可能会争辩说,在这一点上,应该问自己C ++是否是解决手头问题的正确语言。当语言是协作的时候,垃圾回收工作效果最佳(这解锁了许多优化,这些优化在编译器不知道内存发生了什么的情况下是不可行的,就像典型的C或C ++编译器一样)。
请注意,上述任何一种技术都不能让程序员停止思考。即使是垃圾回收器也可能会出现内存泄漏,因为它使用“可达性”作为“将来使用”的近似值(有理论原因表明,在完全一般化的情况下,无法准确检测所有以后不再使用的对象)。您仍然可能需要将某些字段设置为“NULL”,以通知垃圾回收器您将不再通过给定变量访问对象。


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