it-swarm.cn

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

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

122
dulipishi

而不是手动管理内存,尝试使用适当的智能指针。
看一下 Boost libTR1智能指针
智能指针现在也是C++标准的一部分,名为 C++ 11

37
Andri Möll

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

别写了

Object* x = new Object;

甚至

shared_ptr<Object> x(new Object);

什么时候你可以写

Object x;
193
Ross Smith

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

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

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

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

作为开发人员,这非常强大,因为您不需要关心手动资源处理(如在C中所做的那样,或者在Java中使用tryfinally的一些对象)...

编辑(2012-02-12)

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

- _/wilhelmtell

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

那些是 exceptions ways因为C++代码没有乱码,退出等等,或者在有异常的情况下,我们确实需要 unhandled exception 来崩溃进程并且核心转储它的内存图像按原样,而不是清洁后。

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

(谁在休闲的C++代码中调用terminateexit?...我记得在玩 过剩 时必须处理这个问题:这个库非常面向C,只要积极地设计它就可以了对于C++开发人员而言,如果不关心 stack分配的数据 ,或者有关于 永远不会从主循环 返回的“有趣”决策......我将不会对此做出评论

99
paercebal

你会想看看智能指针,比如 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!
}

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

25
Doug T.

阅读 _ raii _ 并确保你理解它。

12
Hank

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

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

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

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

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

11
Jeroen Dirks

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

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

create a thing
use that thing
destroy that thing

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

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

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

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

10
DarenW

好问题!

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

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

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

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

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

  • 可以在 this article中找到很好的资源列表

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

8
ugasoft

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

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

5
Jason Dagit

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

5
fabiopedrosa

用户智能指针无处不在!整类内存泄漏都消失了。

4
DougN

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

4
Seth Morris

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

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

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

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

4
Joseph

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

3
Joseph

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

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

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

2
Seth Morris

按重要性排列的提示:

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

-Tip#2使用RAII

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

-Tip#4不要编写自己的错误Smartpointers,使用boost(在我正在进行的项目上我不能使用boost,而且我不得不调试自己的智能指针,我肯定不会采取再次使用相同的路线,但是现在又一次我不能为我们的依赖项添加提升)

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

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

2
Robert Gould

这些错误的常见来源是当你有一个方法接受一个引用或指向一个对象的指针,但所有权不明确。样式和评论惯例可以降低这种可能性。

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

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

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

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

2
Jonathan

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

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

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

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

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

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

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

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

这完全是关于指针所有权。

1
Null303

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

1
Ronny Brendel

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

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

1
eli

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

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

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

1
Rob

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

0
artificialidiot

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

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();

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

埃玛!

0
Emanuele Oriani

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

0
Nemanja Trifunovic

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

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

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

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

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

你也可以在Visual Studio下尝试 BoundsChecker 这个非常有趣且易于使用。

0
INS

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

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

0
screenglow