确保 C++ 程序中不会泄漏内存的一般技巧有哪些?如何确定谁应该释放动态分配的内存?

有帮助吗?

解决方案

不要手动管理内存,而是在适用的情况下尝试使用智能指针。
看看 增强库, TR1, , 和 智能指针.
智能指针现在也是 C++ 标准的一部分,称为 C++11.

其他提示

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

不要写

Object* x = new Object;

甚至

shared_ptr<Object> x(new Object);

当你可以写的时候

Object x;

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

总结一下(在评论之后 食人魔诗篇33),RAII 依赖于三个概念:

  • 一旦对象被构造出来,它就可以工作了! 一定要在构造函数中获取资源。
  • 对象破坏就足够了! 在析构函数中释放资源。
  • 这都是关于范围的! 作用域对象(参见上面的 doRAIIStatic 示例)将在声明时构造,并在执行退出作用域时被销毁,无论如何退出(返回、中断、异常等)。

这意味着在正确的 C++ 代码中,大多数对象不会用 new, ,并将在堆栈上声明。对于那些使用 new, ,一切都会以某种方式 范围内的 (例如。连接到智能指针)。

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

编辑 (2012-02-12)

“范围对象...将会被破坏...无论出口如何”这并不完全正确。有很多方法可以欺骗 RAII。任何形式的终止()都会绕过清理。exit(EXIT_SUCCESS) 在这方面是一个矛盾的说法。

威廉特尔

威廉特尔 这是非常正确的:有 异常的 欺骗 RAII 的方法,都会导致进程突然停止。

那些是 异常的 方式,因为 C++ 代码中不会充斥着终止、退出等,或者在有异常的情况下,我们确实想要一个 未处理的异常 使进程崩溃并按原样核心转储其内存映像,而不是在清理后。

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

(谁打电话 terminate 或者 exit 在随意的 C++ 代码中?...我记得玩的时候不得不处理这个问题 过剩:这个库非常面向 C,甚至积极地设计它,让 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
}

请注意我的最后一条注释,“当引用计数为零时,这是最酷的部分。因此,如果您的对象有多个用户,则无需跟踪该对象是否仍在使用中。一旦没有人引用您的共享指针,它就会被销毁。

然而,这并不是万能药。尽管您可以访问基指针,但您不会希望将其传递给第 3 方 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!
}

一如既往,用你的思维能力使用任何工具......

继续阅读 RAII 并确保你理解它。

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

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

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

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

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

呸,你们这些小孩子和你们新奇的垃圾收集者……

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

create a thing
use that thing
destroy that thing

有时需要在不同的地方进行创造和破坏;我认为很难避免这种情况。

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

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

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

好问题!

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

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

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

  • 小对象分配由 Alexandrescu 于 2001 年在他的完美著作《现代 c++ 设计》中引入

  • Dimitar Lazarov 撰写的《游戏编程 Gem 7》(2008 年)中一篇名为“高性能堆分配器”的精彩文章中可以找到一个巨大的进步(已分发源代码)

  • 可以在以下位置找到大量资源列表: 文章

不要自己开始编写一个菜鸟无用的分配器......首先记录自己。

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

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

关于如何防止泄漏已经有很多内容,但如果您需要一个工具来帮助您跟踪泄漏,请查看:

尽可能使用智能指针!所有类别的内存泄漏都会消失。

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

瓦尔格林德 也是在运行时检查程序内存泄漏的好工具。

它可在大多数版本的 Linux(包括 Android)和 Darwin 上使用。

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

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

另外,如果有 std 库类(例如向量)。如果您违反了该规则,请确保您拥有虚拟析构函数。

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

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

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

这些错误的常见来源是当您有一个方法接受对象的引用或指针但所有权不明确时。风格和评论约定可以降低这种可能性。

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

在某些情况下使用 const 会有很大帮助。如果函数不会修改对象,并且不会存储返回后仍保留的对该对象的引用,则接受 const 引用。通过阅读调用者的代码,很明显您的函数尚未接受该对象的所有权。您可以让相同的函数接受非常量指针,并且调用者可能会也可能不会假设被调用者接受所有权,但是对于 const 引用,这是没有问题的。

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

我不同意推荐引用计数指针的评论。这通常工作得很好,但是当你有一个错误并且它不起作用时,特别是如果你的析构函数做了一些不平凡的事情,例如在多线程程序中。如果不是太难的话,一定要尝试调整您的设计,使其不需要引用计数。

提示(按重要性排序):

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

-提示#2 使用 RAII

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

-Tip#4 不要编写自己有缺陷的智能指针,使用 boost(在我现在正在进行的一个项目中,我无法使用 boost,而且我不得不调试自己的智能指针,我绝对不会采用再次相同的路线,但现在我无法为我们的依赖项添加提升)

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

-提示#6 找到适合您选择的平台的泄漏检测标头,例如 Visual LeakDetection 的“vld”标头

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

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

当您返回一个shared_ptr时,您就告诉调用者您拥有对它的引用,并且他们拥有部分所有权,但这不仅仅是他们的责任。

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

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

Valgrind 内存检查 是一款优秀的免费软件。

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

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

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

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

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

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

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

这都是关于指针所有权的。

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

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

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

例如检查这个 地点 在C ++中调试内存分配]注意:删除运算符还有一个技巧,如下所示:

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

您可以将文件名存储在某些变量中,并且重载的删除运算符何时会知道从哪个位置调用它。这样您就可以跟踪程序中的每次删除和分配。在内存检查序列结束时,您应该能够报告分配的内存块没有被“删除”,通过文件名和行号来识别它,我猜这就是您想要的。

你也可以尝试类似的东西 边界检查器 在 Visual Studio 下,它非常有趣且易于使用。

我们用一个层包装所有的分配函数,该层在前面附加一个简短的字符串,在末尾附加一个哨兵标志。例如,您可以调用“myalloc( pszSomeString, iSize, iAlignment );或 new(“描述”, iSize) MyObject();它在内部分配指定的大小加上足够的空间来容纳您的标头和哨兵。当然,不要忘记在非调试版本中注释掉它!执行此操作需要多一点内存,但好处远远超过成本。

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

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

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

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++ 内存管理是不是很简单?干杯,

埃玛!

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

任何函数的恰好一个返回。这样你就可以在那里进行释放并且永远不会错过它。

否则很容易犯错误:

new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top