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

什么是一些一般的技巧,以确保我不泄漏在C + +程序内存? 如何确定谁应该释放已dynamic分配的内存?

而不是手动pipe理内存,尽量在适用的地方使用智能指针。
看看Boost lib , TR1和智能指针 。
另外智能指针现在是C ++标准的一部分,称为C ++ 11 。

我完全赞同关于RAII和智能指针的所有build议,但是我也想添加一个稍微高一点的提示:要pipe理的最简单的内存就是你永远不会分配的内存。 不像C#和Java这样的语言,几乎所有的东西都是引用,在C ++中,你应该尽可能的把对象放在堆栈上。 正如我看到有几个人(包括Stroustrup博士)指出的,垃圾收集在C ++中从未stream行的主要原因是编写良好的C ++不会产生太多垃圾。

不要写

Object* x = new Object; 

甚至

 shared_ptr<Object> x(new Object); 

当你可以写的时候

 Object x; 

使用RAII

  • 忘记垃圾收集 (改用RAII)。 请注意,即使垃圾收集器也可能泄漏(如果您忘记在Java / C#中“空”一些引用),并且该垃圾收集器不会帮助您处理资源(如果您有一个对象获得了处理一个文件,如果不用Java手动执行,或者在C#中使用“dispose”模式,那么文件将不会自动释放。
  • 忘记“一个函数返回”规则 。 这是一个很好的避免泄漏的Cbuild议,但是由于使用了exception(使用RAII代替),C ++已经过时了。
  • 虽然“三明治模式”是一个很好的Cbuild议,但由于使用了exception(使用RAII代替) ,C ++已经过时了

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

学习使用智能指针,无论是来自boost,TR1,还是低级的auto_ptr(但是你必须知道它的限制)。

RAII是C ++中exception安全和资源处理的基础,没有其他模式(三明治等)会给你(大多数时候,它不会给你)。

请参阅下面的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

总结(在食人魔诗篇33的评论之后),RAII依靠三个概念:

  • 一旦对象被构build,它就可以工作! 在构造函数中获取资源。
  • 对象破坏就够了! 在析构函数中执行免费资源。
  • 这是关于范围! 范围对象(参见上面的doRAIIStatic示例)将在其声明中构造,并且在执行退出范围时将被销毁,而不pipe退出(返回,中断,exception等)如何。

这意味着,在正确的C ++代码中,大多数对象将不会被构造为new ,而是将被声明在堆栈上。 而对于那些使用new构造的,所有的都会有一定的范围 (例如附加到一个智能指针)。

作为一个开发人员,这是非常强大的,因为您不需要关心手动资源处理(就像在C中完成的那样,或者对于Java中的一些对象,这种情况在这种情况下大量使用try / finally )…

编辑(2012-02-12)

“有范围的对象……将被破坏……不pipe出口是什么”这不完全正确。 有办法欺骗RAII。 任何味道的终止()将绕过清理。 退出(EXIT_SUCCESS)在这方面是一个矛盾。

– wilhelmtell

wilhelmtell的说法是对的:有不同寻常的方式来欺骗RAII,所有这一切都导致程序突然停止。

这些是特殊的方式,因为C ++代码不是终止,退出等,或者在例外的情况下,我们需要一个未处理的exception来崩溃进程和核心转储它的内存图像,而不是清理后。

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

(谁打电话terminateexit的临时C ++代码?…我记得在使用GLUT时必须处理这个问题:这个库是非常面向C的,直到积极地devise它,使像C ++开发人员的困难不关心堆栈分配的数据 ,或者关于永不从主循环返回的 “有趣的”决定……我不会评论这个)

你会想看看智能指针,比如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。 很多时候,你的“发布”东西到其他线程工作完成后,创build范围。 这在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! } 

像往常一样,用任何工具思考上限…

阅读RAII ,并确保您了解它。

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

首先要做的就是在堆栈上进行分配。 这涉及大多数情况下,你需要为某个目的分配一个单一的对象。

如果你确实需要“新”一个对象,那么在大多数情况下,它将有一个明显的所有者在其余的一生中。 对于这种情况,我倾向于使用一堆集合模板,这些模板被devise为通过指针“拥有”存储在其中的对象。 它们与STL向量和映射容器一起实现,但有一些差异:

  • 这些集合不能被复制或分配给。 (一旦它们包含对象)。
  • 指向对象的指针被插入到它们中。
  • 当集合被删除时,首先在集合中的所有对象上调用析构函数。 (我有另一个版本,它声称如果被破坏,而不是空的。)
  • 由于它们存储指针,因此您也可以将inheritance的对象存储在这些容器中。

与STL的关系是,它将注意力集中在Value对象上,而在大多数应用程序中,对象是唯一的实体,在这些容器中使用时没有必要的有意义的复制语义。

呃,你们年轻的孩子和你们新来的垃圾收集者

非常强大的“所有权”规则 – 软件的哪个对象或哪个部分有权删除对象。 清楚的评论和明智的variables名称,以使它明显,如果指针“拥有”或“只是看,不要触摸”。 为了帮助决定谁拥有什么,在每个子程序或方法中尽可能地遵循“三明治”模式。

 create a thing use that thing destroy that thing 

有时需要在广泛不同的地方创造和消灭; 我想很难避免这种情况。

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

当需要访问另一个实体需要访问另一个实体时,需要定义许多其他的指针来扫描数组或其他内容; 这些是“正看”。 对于3D场景示例 – 对象使用纹理但不拥有; 其他物体可以使用相同的纹理。 对象的销毁不会调用任何纹理的破坏。

是的,这是费时,但这就是我所做的。 我很less有内存泄漏或其他问题。 但后来我在高性能科学,数据采集和graphics软件的有限领域工作。 我不经常处理诸如银行和电子商务,事件驱动的graphics用户界面或高度networking化asynchronous混乱等交易。 也许新的方式在那里有优势!

好问题!

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

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

  • 固定大小的分配器在网上随处可见

  • 小对象分配由Alexandrescu于2001年在其完美的书“现代c ++devise”

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

  • 在这篇文章中可以find一个很好的资源列表

不要开始写一个noob无用的分配器自己…首先自己写文档。

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

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

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

用户聪明的指针,无处不在! 整个类的内存泄漏就消失了。

分享并了解整个项目的内存拥有规则。 使用COM规则可以获得最好的一致性([in]参数由调用者拥有,被调用者必须复制; [out]参数由调用者拥有,被调用者在保留引用时必须复制;等等)

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

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

它可用于大多数Linux(包括Android)和达尔文。

如果你使用为你的程序编写unit testing,你应该习惯于系统地运行testing中的valgrind。 它可能会在早期阶段避免许多内存泄漏。 在一个完整的软件中,通过简单的testing来查明它们通常也更容易。

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

如果你不能/不使用智能指针(尽pipe这应该是一个巨大的红旗),请input你的代码:

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

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

这些错误的一个常见的来源是当你有一个方法接受一个对象的引用或指针,但不清楚所有权。 风格和评论惯例可以使这个可能性更小。

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

在一些情况下使用const可以帮助很多。 如果一个函数不会修改一个对象,并且不存储它返回后仍然存在的引用,那么接受一个const引用。 从读取调用者的代码,显然你的函数没有接受对象的所有权。 你可能有相同的函数接受一个非const的指针,调用者可能会也可能不会认为被调用者接受了所有权,但是使用const引用是没有问题的。

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

我不同意推荐引用计数指针的意见。 这通常工作正常,但是当你有一个错误,它不起作用,特别是如果你的析构函数做一些不平凡的,如在一个multithreading程序。 当然不要试图调整你的devise,如果不是太难,就不需要引用计数。

按重要性排列的提示:

– 提示#1永远记得要声明你的析构函数是“虚拟的”。

– 提示#2使用RAII

提示#3使用助推器的智能点

– 提示#4不要写你自己的马车智能指针,使用boost(在一个项目上,我现在不能使用提升,我已经受到debugging我自己的智能指针,我肯定不会采取同样的路线再次,但是现在我再也不能增加我们的依赖)

– 提示#5如果对Thorsten Ottosen的助推指针容器的某些休闲/非性能至关重要(如在有数千个对象的游戏中)

– 提示#6为您所select的平台(例如Visual Leak Detection的“vld”头部)查找泄漏检测标头

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

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

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

这些语义也适用于参数。 如果调用者向你传递一个auto_ptr,他们给你所有权。

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

Valgrind memcheck是一个很好的免费的。

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

 #ifdef _DEBUG #define new DEBUG_NEW #endif 

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

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

如果你要手动pipe理你的记忆,你有两种情况:

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

如果你需要打破这些规则,请logging下来。

这是关于指针的所有权。

你可以截取内存分配函数,看看在程序退出时是否有一些内存区域没有被释放(尽pipe它不适用于所有的应用程序)。

也可以在编译时通过replace运算符new和delete等内存分配函数来完成。

例如,在这个网站签入[在C ++中debugging内存分配]注意:还有一个删除操作符的技巧也是这样的:

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

您可以在某些variables中存储文件的名称,以及重载删除操作符何时会知道从哪个位置调用它。 这样你可以从你的程序中获得每一个delete和malloc的踪迹。 在内存检查序列结束时,您应该能够报告哪些分配的内存块没有被“删除”,通过文件名和行号来标识它,我猜你想要什么。

您也可以在Visual Studio下尝试一些BoundsChecker ,这非常有趣且易于使用。

我们将所有的分配函数换成了一个层,在前面添加一个简短的string,最后添加一个标记标记。 例如,你可以调用“myalloc(pszSomeString,iSize,iAlignment);或new(”description“,iSize)MyObject();它在内部为你的头文件和标记内部分配指定的大小和足够的空间。 ,不要忘了对非debugging版本发表评论!这样做需要更多的内存,但是好处远远大于成本。

这有三个好处 – 首先它允许您快速地跟踪哪些代码泄漏,通过快速search某些“区域”中分配的代码,但是在这些区域应该释放时没有清理。 通过检查确定所有的标记是否完好,检测边界何时被覆盖也是有用的。 当试图find那些隐藏的崩溃或arrays失误时,这已经为我们节省了无数次。 第三个好处是跟踪内存的使用情况,看看大玩家是谁 – 例如,MemDump中的某些描述的sorting规则会告诉您何时“声音”占用的空间比预期的要多。

C ++被devise为RAII。 我觉得用C ++来pipe理内存真的没有更好的办法。 但要小心,不要在本地范围内分配非常大的块(如缓冲区对象)。 它会导致堆栈溢出,如果在使用该块时出现边界检查缺陷,则可以覆盖其他variables或返回地址,从而导致各种安全漏洞。

在不同的地方分配和销毁的唯一例子就是创build线程(你传递的参数)。 但即使在这种情况下也很容易。 这里是创build线程的函数/方法:

 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不是吗? 在线程创build失败的情况下,资源将被auto_ptr释放(删除),否则所有权将被传递给线程。 如果线程如此之快以至于在创build之后释放资源之前呢?

 param.release(); 

被调用的主要function/方法? 没有! 因为我们会'告诉'auto_ptr忽略解除分配。 C ++内存pipe理容易吗? 干杯,

埃玛!

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

  • 尽量避免dynamic分配对象。 只要类有适当的构造函数和析构函数,就可以使用类types的variables,而不是指向它的指针,并且避免dynamic分配和释放,因为编译器会为你做。
    其实这也是“智能指针”所使用的机制,被其他一些作者称为RAII ;-)。
  • 当你将对象传递给其他函数时,比指针更喜欢引用参数。 这避免了一些可能的错误。
  • 在可能的情况下声明参数const,特别是指向对象的指针。 那样的话,对象不能“随意”地被释放(除非你把const放在外面;-)))。
  • 最大限度地减less程序中进行内存分配和释放的地方数量。 例如, 如果您多次分配或释放同一types,请为其写入一个函数(或工厂方法;-))。
    这样,您可以根据需要轻松地创builddebugging输出(哪些地址分配和释放,…)。
  • 使用工厂函数从一个函数分配多个相关类的对象。
  • 如果你的类有一个虚拟析构函数的公共基类,你可以使用相同的函数(或静态方法)释放所有的类。
  • 用像purify这样的工具检查你的程序(不幸的是许多$ /€/ …)。

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

否则很容易犯错误:

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