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


130

有什么常规提示可确保我不会泄漏C ++程序中的内存?我如何确定谁应该释放已经动态分配的内存?


26
对我来说似乎很有建设性。
Shoerob

11
这是建设性的。答案由事实,专业知识,参考资料等支持。并查看支持/答案的数量.. !!
Samitha Chathuranga

Answers:



200

我完全赞同关于RAII和智能指针的所有建议,但是我还想添加一个更高层次的技巧:最容易管理的内存是您从未分配的内存。与C#和Java这样的语言(几乎所有内容都是引用)不同,在C ++中,应尽可能将对象放在堆栈中。正如我看到的一些人(包括Stroustrup博士)所指出的那样,垃圾收集从未在C ++中流行的主要原因是,编写良好的C ++首先不会产生大量垃圾。

不要写

Object* x = new Object;

甚至

shared_ptr<Object> x(new Object);

当你可以写的时候

Object x;

34
我希望我可以给它+10。这是我当今大多数C ++程序员遇到的最大问题,我认为这是因为他们在C ++之前就学习过Java。
克里斯托弗·约翰逊

非常有趣的一点–我想知道为什么C ++内存管理问题的发生频率比其他语言要少得多,但是现在我明白了为什么:它实际上允许东西像香草C一样进入堆栈
。– ArtOfWarfare

那么,如果编写对象x,该怎么办?然后想扔掉x?说x是在main方法中创建的。
Yamcha 2013年

3
@ user1316459 C ++也允许您即时创建作用域。您所要做的就是将x的生存期包装在大括号内,如下所示:{Object x; x.DoSomething; }。在最后一个'}'之后,x的析构函数将被称为释放其包含的所有资源。如果x本身是要在堆上分配的内存,我建议将其包装在unique_ptr中,以便轻松,适当地对其进行清理。
大卫·彼得森

1
罗伯特:是的。罗斯没有说“永远不要写[包含新代码的代码]”,而是说“ 只要可以将其放在堆栈上就不要写”。在大多数情况下,尤其对于性能密集型代码,堆上的大对象将继续是正确的选择。
codetaku 2015年

104

使用RAII

  • 忘记垃圾收集(改为使用RAII)。请注意,即使Garbage Collector也会泄漏(如果您忘记了Java / C#中的某些引用的“空”处理),并且Garbage Collector不会帮助您处理资源(如果您有一个对象获得了处理的权限)一个文件,如果您不使用Java手动操作或使用C#中的“ dispose”模式,则当对象超出范围时,该文件将不会自动释放。
  • 忘记“每个功能一个回报”规则。这是避免泄漏的很好的C语言建议,但是由于使用了异常(因此使用RAII),因此在C ++中已经过时了。
  • 尽管“三明治模式”是不错的C语言建议,但 由于使用了异常(因此改用RAII),因此在C ++中已过时

这篇文章似乎是重复的,但是在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示例)将在它们的声明中构造,并且无论执行如何退出(返回,中断,异常等),执行都会在退出范围时被销毁。

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

作为开发人员,这确实非常强大,因为您无需关心手动资源处理(如在C中完成的操作,或者对于Java中某些需要大量使用try/的对象finally)。

编辑(2012-02-12)

“范围内的对象……将被破坏……无论出口如何”都不是完全正确的。有很多方法可以欺骗RAII。任何类型的terate()都会绕过清理。在这方面,exit(EXIT_SUCCESS)是矛盾的。

威廉姆特

威廉姆勒(Wilhelmtell)对此非常正确:欺骗RAII的方法非常特殊,所有方法都会导致过程突然停止。

这些是特殊的方式,因为C ++代码不会被终止,退出等乱七八糟,或者在有异常的情况下,我们确实希望有一个未处理的异常使进程崩溃并使内核按原样转储内存映像,而不是在清除之后。

但是我们仍然必须知道这些情况,因为尽管它们很少发生,但仍然可能发生。

(谁调用terminateexit使用临时C ++代码?...我记得在使用GLUT时必须处理该问题:该库非常面向C,主动设计它会使C ++开发人员遇到麻烦,例如不关心关于堆栈分配的数据,或关于永不从其主循环返回的 “有趣”决定……我不会对此发表评论


T类不能使用RAII来确保doRAIIStatic()不会泄漏内存吗?例如T p(); p.doSandwich(); 我对此并不十分了解。
Daniel O

@Ogre Psalm33:感谢您的评论。当然,您是对的。我将两个链接都添加到RAII Wikipedia页面,并简要介绍了什么是RAII。
paercebal

1
@Shiftbit:按照优先顺序的三种方式:_ _ _ 1.将真实对象放入STL容器中。_ _ _ 2.将对象的智能指针(shared_ptr)放在STL容器中。_ _ _ 3.将原始指针放在STL容器中,但是包装该容器以控制对数据的任何访问。包装器将确保析构函数将释放分配的对象,包装器访问器将确保在访问/修改容器时没有损坏任何东西。
paercebal

1
@Robert:在C ++ 03中,必须在必须将所有权赋予子函数或父函数(或全局范围)的函数中使用doRAIIDynamic。或者当您通过工厂接收到多态对象的接口时(如果正确编写,则返回智能指针)。在C ++ 11中,情况较少,因为您可以使对象可移动,因此更容易赋予在堆栈上声明的对象所有权...
paercebal 2014年

2
@Robert:...请注意,在堆栈上声明一个对象并不意味着该对象不会在内部使用堆(请注意双重否定... :-) ...)。例如,使用小型字符串优化实现的std :: string将在类的堆栈上有一个用于小型字符串(约15个字符)的缓冲区,并且将使用指向堆中内存的指针来存储较大的字符串...但是从外部来看,std :: string仍然是您在堆栈上声明的值类型(通常),并且您使用的就像使用整数(而不是:对于多态类使用接口)。
paercebal 2014年

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

与往常一样,将您的思维能力与任何工具结合使用...



11

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

首先要做的是在可能的情况下在堆栈上进行分配。在大多数情况下,您需要出于某种目的分配一个对象,这可以解决这个问题。

如果确实需要“新建”一个对象,则在大多数情况下,它在整个生命周期中都只有一个明显的所有者。对于这种情况,我倾向于使用一堆收集模板,这些收集模板设计用于通过指针“拥有”存储在其中的对象。它们是通过STL向量和map容器实现的,但有一些区别:

  • 这些集合不能复制或分配给它们。(一旦它们包含对象。)
  • 指向对象的指针插入其中。
  • 删除集合后,将首先在集合中的所有对象上调用析构函数。(我有另一个版本,该版本断言是否破坏且不为空。)
  • 由于它们存储指针,因此您也可以在这些容器中存储继承的对象。

我对STL的看法是,它非常关注Value对象,而在大多数应用程序中,对象是唯一的实体,不具有在这些容器中使用所需的有意义的复制语义。


10

ah,你年幼的孩子和你新成立的垃圾收集器...

关于“所有权”的非常严格的规则-哪些对象或软件的一部分有权删除该对象。清除注释和明智的变量名称,以使其在指针“拥有”或“只是看起来,不要触摸”时变得明显。为了帮助确定谁拥有什么,请在每个子例程或方法中尽可能遵循“三明治”模式。

create a thing
use that thing
destroy that thing

有时有必要在千差万别的地方创造和破坏;我想避免这种情况。

在任何需要复杂数据结构的程序中,我都会使用“所有者”指针创建一个包含其他对象的严格的清晰对象树。该树为应用程序域概念的基本层次结构建模。例如,一个3D场景拥有对象,灯光,纹理。退出程序时,在渲染结束时,有一种清除所有内容的清晰方法。

每当一个实体需要访问另一个实体,进行扫描或其他任何操作时,就会根据需要定义许多其他指针。这些就是“随便看”。对于3D场景示例-对象使用纹理但不拥有纹理;其他对象可能使用相同的纹理。一个对象的破坏并没有调用任何纹理的破坏。

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


我完全同意。在嵌入式环境中工作,您可能也没有第三方图书馆的奢侈品。
simon

6
我不同意。在“使用该东西”部分中,如果引发了返回或异常,那么您将错过释放。至于性能,std :: auto_ptr不会花费您任何费用。并不是说我从来没有像你那样编码过。只是100%和99%安全代码之间存在差异。:-)
paercebal

8

好问题!

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

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

  • 固定大小分配器在网上无处不在

  • 小对象分配由Alexandrescu于2001年在他的完美著作“现代c ++设计”中提出。

  • 在Dimitar Lazarov撰写的《 Game Programming Gem 7》(2008年)中名为“ High Performance Heap allocator”的高性能文章中,可以找到很大的进步(已分发源代码)。

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

不要自己开始编写noob没用的分配器。


5

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

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



4

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


4

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


4

valgrind也是一个很好的工具,可以在运行时检查程序的内存泄漏。

大多数Linux版本(包括Android)和Darwin都可以使用它。

如果习惯于为程序编写单元测试,则应养成在测试上系统地运行valgrind的习惯。它将有可能在早期避免许多内存泄漏。通常,在完整软件的简单测试中更容易确定它们。

当然,此建议对于任何其他内存检查工具仍然有效。


3

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


2

如果您不能/不使用智能指针进行某些操作(尽管应该是一个巨大的危险信号),请使用以下命令输入代码:

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

这很明显,但是请确保在键入作用域中的任何代码之前先键入它


2

这些错误的一个常见来源是当您拥有一种方法,该方法接受对象的引用或指针,但所有权不清楚。样式和注释约定可以减少这种可能性。

让函数拥有对象所有权的情况为特例。在所有发生这种情况的情况下,请确保在头文件中的函数旁边写一个注释以表明这一点。您应努力确保在大多数情况下,分配对象的模块或类也负责取消分配该对象。

在某些情况下,使用const会很有帮助。如果函数不会修改对象,并且不存储对该对象的引用(在返回后仍然存在),请接受const引用。通过阅读调用者的代码,很明显您的函数尚未接受该对象的所有权。您可能具有相同的函数来接受非const指针,并且调用方可能会也可能不会假定被调用方已接受所有权,但是使用const引用就没有问题。

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

我不同意建议引用计数指针的意见。这通常可以正常工作,但是当您遇到错误并且不起作用时,尤其是在析构函数执行不重要的操作(例如在多线程程序中)的情况下。如果不太难的话,一定要尝试调整您的设计,使其不需要引用计数。


2

重要性提示:

-提示#1始终记得将析构函数声明为“虚拟”。

-提示#2使用RAII

-提示#3使用boost的smartpointer

-提示#4不要编写自己的越野车Smartpointers,使用boost(在我现在正在进行的项目中,我无法使用boost,而且我不得不调试自己的智能指针,我一定不会再次使用相同的路线,但是现在又不能再增加依赖关系了)

-Tip#5如果它对休闲/非性能至关重要(例如在具有数千个对象的游戏中),请查看Thorsten Ottosen的boost指针容器

-Tip#6查找所选平台的泄漏检测头,例如Visual Leak Detection的“ vld”头


我可能错过了一个窍门,但是“游戏”和“非性能关键”又怎么能用同一句话呢?
亚当·内洛

游戏当然是关键场景的一个例子。可能无法在那里弄清楚
罗伯特·古尔德

提示#1仅在类具有至少一个虚方法的情况下才应使用。我绝不会在一个不打算用作多态继承树的基类的类上强加一个无用的虚拟析构函数。
antant

1

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

当您返回auto_ptr时,您就是在告诉调用者您正在给他们内存的所有权。

当您返回shared_ptr时,您是在告诉调用者您对其具有引用,并且它们属于所有权,但这不完全是他们的责任。

这些语义也适用于参数。如果呼叫者将auto_ptr传递给您,则将为您提供所有权。


1

其他人则提到了首先避免内存泄漏的方法(例如智能指针)。但是,一旦有了内存配置文件和内存分析工具,它们通常是跟踪内存问题的唯一方法。

Valgrind memcheck是一款出色的免费软件。


1

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

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

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



1

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

  1. 我创建了对象(可能是通过调用分配新对象的函数间接创建的),然后使用了它(或调用的函数使用了它),然后释放了它。
  2. 有人给了我参考,所以我不应该释放它。

如果您需要违反任何这些规则,请记录下来。

这完全与指针所有权有关。


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

0

您可以拦截内存分配函数,并查看是否有一些内存区域在程序退出时未释放(尽管它并不适合所有应用程序)。

也可以在编译时通过替换new和delete运算符以及其他内存分配函数来完成此操作。

例如,在此站点 [在C ++中调试内存分配]中检查。

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

您可以在一些变量中存储文件的名称,以及何时重载的delete操作符将知道从何处调用文件。这样,您可以跟踪程序中每个删除和malloc的情况。在内存检查序列的末尾,您应该能够报告未“删除”哪些已分配内存块,并通过文件名和行号识别它,这正是您想要的。

您还可以尝试在Visual Studio下尝试诸如BoundsChecker之类的有趣且易于使用的方法。


0

我们将所有分配函数包装在一层,该层在前面附加一个简短的字符串,在末尾附加一个哨兵标志。因此,例如,您将调用“ myalloc(pszSomeString,iSize,iAlignment);或new(“ description”,iSize)MyObject();这将在内部分配指定的大小以及足够的空间用于标题和哨兵。 ,别忘了将其用于非调试版本,将其注释掉,这样做需要更多的内存,但其好处远大于成本。

这具有三个好处-首先,通过快速搜索在某些“区域”中分配的代码,而在这些区域应该释放时不进行清理,可以轻松快速地跟踪泄漏的代码。通过检查以确保所有标记均完好无损来检测边界何时被覆盖也很有用。在寻找那些隐秘的崩溃或阵列失误时,这为我们节省了很多时间。第三个好处是跟踪内存使用情况以查看谁是大玩家-例如,MemDump中某些描述的整理可以告诉您“声音”何时比您预期的占用更多的空间。


0

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


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

很简单,不是吗?万一线程创建失败,资源将由auto_ptr释放(删除),否则所有权将传递给线程。如果线程如此之快以至于在创建之后释放线程资源,那该怎么办?

param.release();

在主函数/方法中被调用?没有!因为我们将“告诉” auto_ptr忽略释放。C ++内存管理容易吗?干杯,

埃玛!



-3

从任何函数返回一个正好。这样一来,您就可以在那里进行释放,而永远不会错过它。

否则容易出错:

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

您的答案与此处的示例代码不符吗?我同意答案“只有一次回报”,但是示例代码显示了不应该做的事情。
simon

1
C ++ RAII的目的恰恰是避免您编写的那种代码。在C语言中,这可能是正确的选择。但是在C ++中,您的代码有缺陷。例如:如果new b()抛出该怎么办?你漏了。
paercebal
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.