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

127 c++ memory memory-management raii

有哪些一般提示可以确保我不会在C++程序中泄漏内存?我如何确定谁应该释放已动态分配的内存?

小智 197

我完全赞同关于RAII和智能指针的所有建议,但我还想添加更高级别的提示:最容易管理的内存是你从未分配过的内存.与C#和Java等语言不同,几乎所有东西都是引用,在C++中,你应该尽可能地将对象放在堆栈上.正如我看到几个人(包括Dr Stroustrup)指出的那样,垃圾收集从未在C++中流行的主要原因是编写良好的C++首先不会产生太多垃圾.

别写了

Object* x = new Object;
Run Code Online (Sandbox Code Playgroud)

甚至

shared_ptr<Object> x(new Object);
Run Code Online (Sandbox Code Playgroud)

什么时候你可以写

Object x;
Run Code Online (Sandbox Code Playgroud)

  • 我希望我能给这个+10.这是我今天对大多数C++程序员看到的最大问题,我认为这是因为他们在C++之前学习了Java. (33认同)
  • @ user1316459 C++允许您动态创建范围.你需要做的就是将x的生命周期包裹在大括号中,如下所示:{Object x; x.DoSomething; }.在最后的'}'之后,x的析构函数将被称为释放它包含的任何资源.如果x本身是要在堆上分配的内存,我建议将其包装在unique_ptr中,以便轻松适当地清理它. (3认同)

pae*_*bal 102

使用RAII

  • 忘记垃圾收集(改用RAII).请注意,即使垃圾收集器也可能泄漏(如果您忘记在Java/C#中"空"某些引用),并且垃圾收集器将无法帮助您处理资源(如果您有一个获取句柄的对象如果您不在Java中手动执行该操作,或者在C#中使用"dispose"模式,则当该对象超出范围时,该文件将不会自动释放.
  • 忘记"每个功能一次返回"规则.这是一个很好的C建议,以避免泄漏,但它在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.
}
Run Code Online (Sandbox Code Playgroud)

关于RAII

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

  • 一旦构造了对象,它就会起作用!在构造函数中获取资源.
  • 对象破坏就足够了!在析构函数中释放资源.
  • 一切都与范围有关!Scoped对象(参见上面的doRAIIStatic示例)将在其声明中构造,并且在执行退出作用域时将被销毁,无论退出(返回,中断,异常等)如何.

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

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

编辑(2012-02-12)

"范围内的物体......将被破坏......无论退出",这都不完全正确.有办法欺骗RAII.任何flavor()都会绕过清理.退出(EXIT_SUCCESS)是这方面的矛盾.

- 威廉姆特尔

wilhelmtell是关于完全正确:有特殊的方法来欺骗RAII,所有通往过程突然停止.

这些都是特殊的方式,因为C++代码不会出现终止,退出等问题,或者在异常的情况下,我们确实需要一个未处理的异常来使进程崩溃并且核心转储其内存映像,而不是在清理之后.

但我们仍然必须了解这些案例,因为虽然它们很少发生,但它们仍然可以发生.

(谁调用terminateexit使用随意的C++代码?...我记得在使用GLUT时必须处理这个问题:这个库是非常面向C的,只要积极地设计它就会让C++开发人员感到困难,比如不关心关于堆栈分配的数据,或者有关于永远不会从主循环返回的 "有趣"决定...我不会对此发表评论.

  • @Robert:...注意,在栈上声明一个对象并不意味着该对象不在内部使用堆(注意双重否定...... :-) ......).例如,使用Small String Optimization实现的std :: string将为小字符串(~15个字符)提供一个"在类的堆栈上"的缓冲区,并且将使用指向堆中的内存的指针来获取更大的字符串...但是从外部来看,std :: string仍然是一个你在堆栈上声明(通常)的值类型,你可以像使用整数一样使用(而不是:因为你会使用一个接口来实现多态类). (2认同)

And*_*öll 39

不要手动管理内存,而是尝试在适用的地方使用智能指针.
看一下Boost lib,TR1智能指针.
智能指针现在也是C++标准的一部分,称为C++ 11.


Dou*_* T. 25

你会想看看智能指针,比如boost的智能指针.

代替

int main()
{ 
    Object* obj = new Object();
    //...
    delete obj;
}
Run Code Online (Sandbox Code Playgroud)

一旦引用计数为零,boost :: shared_ptr将自动删除:

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

请注意我的最后一个注释,"当引用计数为零时,这是最酷的部分.因此,如果您拥有对象的多个用户,则无需跟踪对象是否仍在使用中.一旦没有人引用您的共享指针,它被破坏了.

然而,这不是灵丹妙药.虽然您可以访问基本指针,但您不希望将其传递给第三方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!
}
Run Code Online (Sandbox Code Playgroud)

一如既往,使用任何工具的思维上限......


小智 12

阅读RAII并确保您理解它.


Jer*_*rks 11

大多数内存泄漏是由于不清楚对象所有权和生命周期.

首先要做的是尽可能在Stack上进行分配.这涉及大多数需要为某些目的分配单个对象的情况.

如果你确实需要"新"一个对象,那么大多数时候它将在其整个生命周期中拥有一个明显的所有者.对于这种情况,我倾向于使用一堆集合模板,这些模板设计用于通过指针"拥有"存储在其中的对象.它们是使用STL向量和映射容器实现的,但有一些区别:

  • 无法复制或分配这些集合.(一旦它们包含对象.)
  • 指向对象的指针插入其中.
  • 删除集合时,首先在集合中的所有对象上调用析构函数.(我有另一个版本,如果被破坏而不是空的,它会断言.)
  • 由于它们存储指针,您还可以将继承的对象存储在这些容器中.

我对STL的关注是它专注于Value对象,而在大多数应用程序中,对象是唯一的实体,没有在这些容器中使用所需的有意义的复制语义.


Dar*_*enW 10

呸,你们年幼的孩子和你那些新奇的垃圾收集者......

关于"所有权"的非常强有力的规则 - 软件的哪个对象或部分有权删除该对象.明确的注释和明智的变量名称,以便在指针"拥有"或"只是看,不要触摸"时显而易见.为了帮助确定谁拥有什么,尽可能遵循每个子程序或方法中的"三明治"模式.

create a thing
use that thing
destroy that thing
Run Code Online (Sandbox Code Playgroud)

有时需要在广泛不同的地方创造和销毁; 我觉得很难避免这种情况.

在任何需要复杂数据结构的程序中,我使用"所有者"指针创建一个包含其他对象的严格明确的对象树.此树模拟应用程序域概念的基本层次结构.示例3D场景拥有对象,灯光,纹理.程序退出时渲染结束时,有一种清除方法可以摧毁一切.

每当一个实体需要访问另一个实体,扫描数组或其他内容时,就会根据需要定义许多其他指针; 这些都是"正在寻找".对于3D场景示例 - 对象使用纹理但不拥有; 其他对象可能使用相同的纹理.对象的破坏不会引起任何纹理的破坏.

是的,这是耗时的,但这就是我的工作.我很少有内存泄漏或其他问题.但后来我在有限的高性能科学,数据采集和图形软件领域工作.我不经常处理银行和电子商务,事件驱动的GUI或高网络异步混乱等交易.也许那些新奇的方式在那里有优势!

  • 我不同意.在"使用那个东西"的部分,如果抛出一个返回或异常,那么你将错过重新分配.至于性能,std :: auto_ptr会花费你什么.并不是说我从不以同样的方式编写代码.只是100%和99%的安全代码之间存在差异.:-) (6认同)

uga*_*oft 8

好问题!

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

我认为你能做的更好的是合并各种作者的一些有趣的作品,我可以给你一些提示:

  • 固定大小分配器在网络中随处可见

  • Alexandrescu在2001年的完美着作"现代c ++设计"中介绍了小对象分配

  • 在Dimitar Lazarov编写的游戏编程宝石7(2008)中名为"高性能堆分配器"的惊人文章中可以找到一个伟大的进步(源代码分发)

  • 一种很大的资源列表中可以找到这个文章

不要自己开始写一个noob unuseful allocator ...首先记录你自己.


Jas*_*git 5

一种在C++中受到内存管理欢迎的技术是RAII.基本上,您使用构造函数/析构函数来处理资源分配.当然,由于异常安全,C++中还有其他一些令人讨厌的细节,但基本思路非常简单.

这个问题通常归结为所有权问题.我强烈推荐阅读Scott Meyers的Effective C++系列和Andrei Alexandrescu的Modern C++ Design.


fab*_*osa 5

关于如何不泄漏已经有很多,但如果你需要一个工具来帮助你跟踪泄漏,请看看: