C ++中避免内存泄漏的一般准则

132

有什么一般性质的技巧可以确保我在C++程序中不会泄漏内存? 我如何确定谁应该释放动态分配的内存?


26
听起来对我来说非常有建设性。 - Shoerob
11
这是有建设性的。而且回答受到事实、专业知识、参考资料等的支持。看看点赞数/回答数量..!! - Samitha Chathuranga
29个回答

204

我完全赞同关于RAII和智能指针的所有建议,但我还想补充一个稍微高级一点的提示:最容易管理的内存是你从未分配的内存。与C#和Java等语言不同,在C++中,只要有可能,你应该将对象放在堆栈上。正如我看到过的几个人(包括Stroustrup博士)指出的那样,垃圾回收在C++中从来没有流行的主要原因是,良好编写的C++代码本身就不会产生太多垃圾。

不要这样写

Object* x = new Object;

甚至

shared_ptr<Object> x(new Object);

当你可以直接写的时候

Object x;

35
我希望我能给这个评分+10。这是我今天看到大多数C++程序员最大的问题,我认为这是因为他们在学习C++之前学习了Java。 - Kristopher Johnson
1
非常有趣的观点 - 我曾经想知道为什么我在C++内存管理方面遇到的问题比其他语言少得多,但现在我明白了:它实际上允许像普通C一样将东西放在堆栈中。 - ArtOfWarfare
1
如果你写了Object x;,然后想要丢弃x,该怎么办呢?比如说x是在主方法中创建的。 - Yamcha
5
C++允许你动态地创建作用域。你只需要像下面这样使用大括号将x的生命周期包含在内:{ Object x; x.DoSomething; }。在最后一个“}”之后,x的析构函数会被调用以释放其所包含的任何资源。如果x本身是要在堆上分配的内存,我建议将其包装在unique_ptr中,以便轻松而适当地清理。 - David Peterson
1
罗伯特:是的。罗斯没有说“永远不要编写包含new的代码”,他说“当你可以只是将其放在堆栈上时,不要编写那样的代码”。对于大型对象,将其放在堆上仍然是大多数情况下的正确选择,尤其是对于性能密集型代码。 - codetaku
显示剩余4条评论

106

使用RAII

  • 忘记垃圾回收(使用RAII代替)。请注意,即使垃圾回收器也可能泄漏(如果您忘记在Java/C#中将某些引用“null”),并且垃圾回收器不会帮助您处理资源(如果您有一个对象获取了文件的句柄,则当对象超出范围时文件不会自动释放,如果您不在Java中手动执行此操作或在C#中使用“dispose”模式)。
  • 忘记“每个函数只返回一个值”的规则。这是一个避免泄漏的好建议,但在C++中已过时,因为它使用了异常(使用RAII代替)。
  • “三明治模式”是一个好的C建议,但由于C++使用了异常,所以已经过时(使用RAII代替)。

这篇文章似乎有点重复,但在C++中,最基本的模式是RAII

学习使用智能指针,来自boost、TR1甚至是低效但通常足够的auto_ptr(但您必须知道其限制)。

RAII是C++中异常安全和资源处理的基础,没有其他模式(三明治等)会同时提供这两个功能(大多数情况下,不会提供任何功能)。

请参见以下RAII和非RAII代码的比较:

void doSandwich()
{
   T * p = new T() ;
   // do something with p
   delete p ; // leak if the p processing throws or return
}

void doRAIIDynamic()
{
   std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

void doRAIIStatic()
{
   T p ;
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

关于 RAII

总结一下(来自Ogre Psalm33的评论后),RAII依赖于三个概念:

  • 对象构建后即可正常工作!在构造函数中获取资源。
  • 仅需对象销毁!在析构函数中释放资源。
  • 全部与作用域有关!作用域对象(如上面的doRAIIStatic示例)将在声明时被构造,并在执行退出作用域时销毁,无论退出方式(返回、break、异常等)。

这意味着,在正确的C++代码中,大多数对象不会使用new构造,而是在堆栈上声明。对于那些使用new构造的对象,都将以某种方式作用域化(例如附加到智能指针)。

作为开发人员,这确实非常强大,因为您不需要关心手动资源处理(如在C中或Java中一些对象的情况下大量使用try/finally)...

编辑(2012-02-12)

“作用域对象...将被销毁...无论退出方式”并不完全正确。有方法可以欺骗RAII。任何一种终止()方式都将绕过清理。在这方面,exit(EXIT_SUCCESS)是一个自相矛盾的说法。

wilhelmtell

威廉·泰尔是正确的:有一些 特殊 的方法可以欺骗 RAII,但这些方法最终都会导致进程突然停止。

这些方法是 特殊 的,因为 C++ 代码并不会充斥着 terminate、exit 等等,或者在异常情况下,我们确实希望 未处理的异常 使进程崩溃,并在清理之前转储其内存映像。

但我们仍然必须了解这些情况,因为虽然它们很少发生,但仍然可能发生。

(谁会在普通的 C++ 代码中调用 terminateexit 呢?……我记得当我玩 GLUT 时,就不得不处理这个问题:这个库非常面向 C,甚至特意设计得让 C++ 开发人员感到困难,比如不关心 堆栈分配数据,或者对于 从主循环中永远不返回 的“有趣”决策……我不想评论这个)


太棒了...一流! :-) - Ogre Psalm33
1
@Shiftbit:按优先顺序有三种方法:_ _ _ 1. 将真实对象放入STL容器中。_ _ _ 2. 将对象的智能指针(shared_ptr)放入STL容器中。_ _ _ 3. 将原始指针放入STL容器中,但包装容器以控制对数据的任何访问。包装器将确保析构函数释放分配的对象,并且包装器访问器将确保在访问/修改容器时不会破坏任何内容。 - paercebal
你何时会使用 doRAIIDynamic 而不是 doRAIIStatic?如果我没记错的话,静态情况下对象是在堆上分配的,所以如果 T 对象很大,你会想要使用动态版本,对吗? - Robert
1
@Robert:在C++03中,您可以在必须将所有权交给子函数、父函数(或全局范围)的函数中使用doRAIIDynamic。或者当您通过工厂接收多态对象的接口时(如果正确编写,则返回智能指针)。在C++11中,情况有所不同,因为您可以使对象可移动,因此更容易将声明在堆栈上的对象的所有权转移。 - paercebal
2
@Robert:请注意,在堆栈上声明对象并不意味着该对象不在内部使用堆(请注意双重否定... :-))。例如,使用小字符串优化实现的std::string将在类的堆栈上具有用于小字符串(〜15个字符)的缓冲区,并且将对较大字符串使用指向堆中内存的指针...但从外部来看,std::string仍然是一个值类型,您可以像使用整数一样在堆栈上声明并使用它(与多态类的接口使用方式相反)。 - paercebal
显示剩余7条评论

41

尽可能使用智能指针来管理内存,避免手动管理。
可以查看 Boost libTR1智能指针
此外,智能指针现在已成为名为 C++11 的 C++ 标准的一部分。


1
使用g++编译需要添加参数:-std=c++0x。 - Paweł Szczur
你可以使用标志值 -std=c++11 通过 g++ 进行编译。 - Prabhash Rathore

25

您需要查看智能指针,例如boost的智能指针

与其

int main()
{ 
    Object* obj = new Object();
    //...
    delete obj;
}

当引用计数为零时,boost::shared_ptr将自动删除:

int main()
{
    boost::shared_ptr<Object> obj(new Object());
    //...
    // destructor destroys when reference count is zero
}

请注意我上一条备注中提到的,“当引用计数归零时,这是最酷的部分。因此,如果您的对象有多个用户,则无需跟踪对象是否仍在使用中。一旦没有人引用您的共享指针,它将被销毁。

然而,这并不是万能的。虽然您可以访问基本指针,但除非您对其所做的操作感到自信,否则不要将其传递给第三方API。在Win32中,PostThreadMessage通常在创建范围结束后将任务“发布”到另一个线程中进行处理:

void foo()
{
   boost::shared_ptr<Object> obj(new Object()); 

   // Simplified here
   PostThreadMessage(...., (LPARAM)ob.get());
   // Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}

像往常一样,在使用任何工具时都要动动脑筋...


12

阅读关于RAII的内容,并确保您理解它。


11

啊,你们这些年轻人总是喜欢用新式垃圾回收器...

非常严格的"所有权"规则 - 哪个对象或软件部分有权删除对象。清晰的注释和明智的变量名,以使指针是否"拥有"或"只看不碰"显而易见。为了帮助决定谁拥有什么,在每个子程序或方法中尽可能地遵循"三明治"模式。

create a thing
use that thing
destroy that thing

有时需要在不同的位置创建和销毁对象,难以避免。在任何需要复杂数据结构的程序中,我会创建一个严格清晰、包含其他对象的对象树-使用“owner”指针。这个树形结构模拟应用领域概念的基本层次结构。例如,一个3D场景拥有对象、灯光、纹理。在渲染结束并退出程序时,有明确的方法销毁所有内容。

当一个实体需要访问另一个实体、扫描数组或者其他情况下,我会定义需要的其他指针;这些是“只读”的。对于3D场景的例子来说,一个对象使用了纹理但并不拥有它;其他对象可能也会使用相同的纹理。对象的销毁不会触发任何纹理的销毁。

虽然这很费时间,但我经常这样做。我很少出现内存泄漏或其他问题。但是我工作在高性能科学、数据采集和图形软件的有限领域。我很少处理银行和电子商务交易、事件驱动的GUI或高度网络化的异步混沌。也许新潮的方式在那里有优势!


我完全同意。在嵌入式环境中工作,您可能也没有第三方库的奢侈条件。 - simon
6
我不同意。在“使用那个东西”的部分,如果发生返回或异常抛出,那么您将错过释放内存的机会。至于性能方面,std::auto_ptr不会对您造成任何开销。并非我从未像您那样编写代码。只是100%安全和99%安全的代码之间存在差异。 :-) - paercebal

11

大多数内存泄漏都是由于不清楚对象所有权和生命周期而导致的。

第一件事情是尽可能在栈上分配。这解决了大多数需要为某些目的分配单个对象的情况。

如果您确实需要使用 'new' 来创建对象,那么在其余的生命周期中,它往往会有一个明显的所有者。对于这种情况,我倾向于使用一堆集合模板,它们被设计用于通过指针拥有其中存储的对象。它们是使用STL vector和map容器实现的,但有一些区别:

  • 这些集合无法复制或赋值。(一旦它们包含对象)
  • 指向对象的指针被插入到它们中。
  • 删除集合时,首先调用集合中所有对象的析构函数。(我还有另一个版本,在析构并非空时它会断言。)
  • 由于它们存储指针,因此您也可以在这些容器中存储继承对象。

我对STL的看法是,它过于专注于值对象,而在大多数应用程序中,对象是独特的实体,它们没有必要在这些容器中使用所需的有意义的复制语义。


8

很棒的问题!

如果您正在使用C ++并且正在开发实时CPU和内存绑定应用程序(如游戏),则需要编写自己的内存管理器。

我认为您可以将各种作者的一些有趣作品合并起来,我可以给您一些提示:

  • 固定大小的分配器在网络上被广泛讨论

  • Alexandrescu在他的完美著作“现代C ++设计”中于2001年介绍了小对象分配

  • 在Dimitar Lazarov撰写的Game Programming Gem 7(2008)的一篇惊人文章“高性能堆分配器”中可以找到一个伟大的进步(带有源代码分发)

  • 您可以在文章中找到资源的伟大列表

不要开始编写无用的新手分配器... 首先要进行文档化。


5

在C++中,已经变得越来越流行的一种内存管理技术是RAII。通常利用构造函数和析构函数来处理资源分配。 当然,在C++中有一些其他令人讨厌的细节,由于异常安全性,但基本思想还是相当简单的。

问题通常归结为所有权问题。我强烈建议阅读Scott Meyers的Effective C++系列和Andrei Alexandrescu的Modern C++ Design。


5

BoundsChecker 出现了 404 错误。 - TankorSmash

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