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

时间:2008-09-16 20:41:15

标签: c++ memory memory-management raii

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

29 个答案:

答案 0 :(得分:193)

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

不要写

Object* x = new Object;

甚至

shared_ptr<Object> x(new Object);

什么时候可以写

Object x;

答案 1 :(得分:99)

使用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.
}

关于RAII

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

  • 一旦构造了对象,它就可以了!在构造函数中获取资源。
  • 对象销毁就足够了!在析构函数中使用免费资源。
  • 关于范围的全部内容! Scoped对象(参见上面的doRAIIStatic示例)将在其声明中构造,并且在执行退出范围时将被销毁,无论退出如何(返回,休息,例外等。)。

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

作为一名开发人员,这非常强大,因为您不需要关心手动资源处理(如在C中所做的那样,或者在Java中使用try / {{1对于那种情况)...

编辑(2012-02-12)

  

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

     

- wilhelmtell

wilhelmtell对此非常正确:有例外欺骗RAII的方法,所有这些都会导致进程突然停止。

这些是特殊方式,因为C ++代码不会出现终止,退出等问题,或者在出现异常的情况下,我们确实希望unhandled exception使进程和核心崩溃按原样转储内存映像,而不是清理后。

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

(谁在休闲的C ++代码中调用finallyterminate?...我记得在玩GLUT时必须处理这个问题:这个库非常C面向导向,主动设计它使C ++开发人员难以理解stack allocated data,或者对never returning from their main loop做出“有趣”的决定......我不会对此发表评论

答案 2 :(得分:37)

不要手动管理内存,而是尝试在适用的地方使用智能指针 请查看Boost libTR1smart pointers 智能指针现在也是C ++标准的一部分,名为C++11

答案 3 :(得分:25)

您需要查看智能指针,例如boost's smart pointers

而不是

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!
}

与往常一样,使用任何工具的思维上限......

答案 4 :(得分:12)

阅读RAII并确保您理解它。

答案 5 :(得分:11)

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

要做的第一件事就是尽可能在Stack上进行分配。这涉及大多数需要为某种目的分配单个对象的情况。

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

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

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

答案 6 :(得分:10)

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

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

create a thing
use that thing
destroy that thing

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

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

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

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

答案 7 :(得分:8)

很棒的问题!

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

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

  • 大量讨论固定大小分配器,网络无处不在

  • 小对象分配是由Alexandrescu于2001年在他的完美着作“现代c ++设计”中引入的

  • 在Dimitar Lazarov编写的Game Programming Gem 7(2008)中一篇名为“High Performance Heap allocator”的精彩文章中可以找到一个很棒的进步(源代码已经发布)

  • 可以在this文章

  • 中找到很好的资源列表

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

答案 8 :(得分:5)

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

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

答案 9 :(得分:5)

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

答案 10 :(得分:4)

用户智能指针随处可见!整个类别的内存泄漏都消失了。

答案 11 :(得分:4)

valgrind 也是在运行时检查程序内存泄漏的好工具。

它适用于大多数Linux(包括Android)和Darwin。

如果您使用为您的程序编写单元测试,您应该养成在测试中系统运行valgrind的习惯。它可能会在早期阶段避免许多内存泄漏。在完整软件中的简单测试中,通常也更容易确定它们。

当然,这个建议对任何其他内存检查工具都有效。

答案 12 :(得分:4)

在整个项目中共享并了解内存所有权规则。使用COM规则可以实现最佳一致性([in]参数由调用者拥有,被调用者必须复制; [out] params由调用者拥有,被调用者必须在保留引用时复制;等等。)

答案 13 :(得分:3)

另外,如果有std库类(例如vector),请不要使用手动分配的内存。确保您违反了具有虚拟析构函数的规则。

答案 14 :(得分:2)

如果您不能/不使用智能指针(虽然这应该是一个巨大的红旗),请输入您的代码:

allocate
if allocation succeeded:
{ //scope)
     deallocate()
}

这是显而易见的,但请确保在之前输入键入范围内的任何代码

答案 15 :(得分:2)

按重要性排列的提示:

-Tip#1永远记住要将你的析构函数声明为“虚拟”。

-Tip#2使用RAII

-Tip#3使用boost的智能指针

-Tip#4不要编写你自己的越野车Smartpointers,使用boost(在我正在进行的项目上,我不能使用boost,我不得不调试我自己的智能指针,我会肯定不再采取相同的路线,但现在再次,我不能添加对我们的依赖的推动)

-Tip#5如果它的一些随意/非性能关键(如在具有数千个对象的游戏中)工作,请查看Thorsten Ottosen的提升指针容器

-Tip#6为您选择的平台找到泄漏检测标题,例如Visual Leak Detection的“vld”标题

答案 16 :(得分:2)

这些错误的常见原因是当你有一个方法接受一个对象的引用或指针但是所有权不明确时。风格和评论惯例可能会降低这种可能性。

让函数获取对象的所有权的情况是特例。在发生这种情况的所有情况下,请务必在头文件中的函数旁边写一个注释来指示这一点。您应该努力确保在大多数情况下,分配对象的模块或类也负责解除分配。

在某些情况下,使用const可以提供很多帮助。如果函数不修改对象,并且不存储对其返回后仍然存在的引用,则接受const引用。通过阅读调用者的代码,您的函数显然不会接受对象的所有权。您可以使用相同的函数接受非const指针,并且调用者可能会或可能不会假定被调用者接受所有权,但使用const引用则毫无疑问。

不要在参数列表中使用非const引用。在读取调用者代码时,非常不清楚被调用者可能保留了对参数的引用。

我不同意推荐引用计数指针的评论。这通常可以正常工作,但是当你有一个bug并且它不起作用时,特别是如果你的析构函数做了一些非常重要的事情,比如在多线程程序中。如果不太难,请务必将设计调整为不需要引用计数。

答案 17 :(得分:1)

valgrind(仅适用于* nix平台)是一个非常好的内存检查器

答案 18 :(得分:1)

仅对于MSVC,将以下内容添加到每个.cpp文件的顶部:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

然后,当使用VS2003或更高版本进行调试时,您将被告知程序退出时的任何泄漏(它跟踪新/删除)。这是基本的,但它在过去帮助了我。

答案 19 :(得分:1)

  • 尽量避免动态分配对象。只要类具有适当的构造函数和析构函数,使用类类型的变量,而不是指向它的指针,并且您避免动态分配和释放,因为编译器将为您执行此操作。
    实际上,这也是“智能指针”使用的机制,并被其他一些作家称为RAII ;-)。
  • 将对象传递给其他函数时,首选参数而不是指针。这避免了一些可能的错误。
  • 尽可能声明参数const,尤其是指向对象的指针。这样就无法“意外地”释放对象(除非你把const扔掉;-)))。
  • 最小化程序中进行内存分配和释放的位置数。 E. g。如果您多次分配或释放相同的类型,请为其编写一个函数(或工厂方法;-)) 这样,您可以根据需要轻松创建调试输出(分配和解除分配地址,...)。
  • 使用工厂函数从单个函数分配多个相关类的对象。
  • 如果您的类具有带虚拟析构函数的公共基类,则可以使用相同的函数(或静态方法)释放所有类。
  • 使用purify等工具检查您的程序(不幸的是很多$ /€/ ...)。

答案 20 :(得分:1)

其他人已经提到过首先避免内存泄漏的方法(比如智能指针)。但是,分析和内存分析工具通常是一旦有内存问题就可以追踪内存问题的唯一方法。

Valgrind memcheck是一个优秀的免费版。

答案 21 :(得分:1)

如果您要手动管理内存,则有两种情况:

  1. 我创建了对象(可能通过调用分配新对象的函数间接创建),我使用它(或者我调用的函数使用它),然后我释放它。
  2. 有人给了我参考,所以我不应该释放它。
  3. 如果您需要违反任何这些规则,请记录下来。

    关于指针所有权。

答案 22 :(得分:1)

如果可以,请使用boost shared_ptr和标准C ++ auto_ptr。那些传达了所有权语义。

当你返回一个auto_ptr时,你告诉调用者你正在给他们内存的所有权。

当你返回一个shared_ptr时,你告诉调用者你有一个引用它并且他们参与了所有权,但这不仅仅是他们的责任。

这些语义也适用于参数。如果来电者通过auto_ptr,他们会给你所有权。

答案 23 :(得分:0)

关于在不同位置分配和销毁的唯一示例之一是线程创建(您传递的参数)。 但即使在这种情况下也很容易。 这是创建线程的函数/方法:

struct myparams {
int x;
std::vector<double> z;
}

std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...

这里代替线程函数

extern "C" void* th_func(void* p) {
   try {
       std::auto_ptr<myparams> param((myparams*)p);
       ...
   } catch(...) {
   }
   return 0;
}

漂亮的easyn不是吗?如果线程创建失败,auto_ptr将释放(删除)资源,否则所有权将传递给线程。 如果线程如此之快以至于在创建之后它会在

之前释放资源
param.release();
在主函数/方法中调用

?没有!因为我们将“告诉”auto_ptr忽略释放。 难道C ++内存管理不容易吗? 欢呼声,

的Ema!

答案 24 :(得分:0)

C ++在设计时考虑了RAII。我认为没有更好的方法来管理C ++中的内存。 但要注意不要在本地范围内分配非常大的块(如缓冲区对象)。它可能导致堆栈溢出,如果在使用该块时边界检查中存在缺陷,则可以覆盖其他变量或返回地址,这会导致各种安全漏洞。

答案 25 :(得分:0)

我们将所有分配函数包装在一个层,该层在前面附加一个简短的字符串,在末尾附加一个sentinel标志。例如,你有一个调用“myalloc(pszSomeString,iSize,iAlignment);或者新的(”描述“,iSize)MyObject();它在内部为你的标题和标记分配指定的大小和足够的空间。当然,不要忘记对非调试版本进行评论!这需要更多的内存来实现这一点,但其好处远大于成本。

这有三个好处 - 首先它允许您通过快速搜索在某些“区域”中分配的代码来轻松快速地跟踪泄漏的代码,但在这些区域应该被释放时不会被清除。通过检查以确保所有标记完整无缺,检测边界何时被覆盖也是有用的。在试图找到那些隐藏得很严重的崩溃或阵列失误时,这已经为我们节省了很多次。第三个好处是跟踪内存的使用,看看大玩家是谁 - 例如,MemDump中某些描述的整理会告诉你'声音'占用的空间比你预期的多。

答案 26 :(得分:0)

您可以拦截内存分配函数,看看是否有一些内存区域在程序退出时没有释放(虽然它不适合 all 应用程序)。

它也可以在编译时通过替换operator new和delete以及其他内存分配函数来完成。

例如,检查此site [在C ++中调试内存分配] 注意:删除操作符有一个技巧,如下所示:

#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE

您可以在某些变量中存储文件的名称,以及重载的删除操作符何时知道调用它的位置。这样,您就可以从程序中获取每个delete和malloc的跟踪。在内存检查序列结束时,您应该能够报告已分配的内存块未被“删除”,通过文件名和行号识别它,这就是我想你想要的内容。

您还可以在Visual Studio下尝试类似BoundsChecker的内容,这非常有趣且易于使用。

答案 27 :(得分:0)

以与管理其他资源(句柄,文件,数据库连接,套接字......)相同的方式管理内存。 GC也不会帮助你。

答案 28 :(得分:-3)

任何函数都可以返回一个。这样你就可以在那里进行解除分配而不会错过它。

否则很容易犯错:

new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.