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

在 C + + 程序中,有什么一般性的技巧可以确保我不会泄漏内存?如何确定谁应该释放已经动态分配的内存?

123619 次浏览

用户智能指针无处不在! 所有类的内存泄漏都会消失。

您需要查看智能指针,例如 Boost 的智能指针

而不是

int main()
{
Object* obj = new Object();
//...
delete obj;
}

一旦引用计数为零,share _ 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!
}

像往常一样,使用你的思考帽与任何工具..。

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

不要手动管理内存,而是尝试在适当的地方使用智能指针。
看看 推动自由TR1聪明的指点
此外,智能指针现在是 C + + 标准 C + + 11的一部分

仔细阅读 拉尔,确保你理解它。

如果可以的话,使用 Boost share _ ptr 和标准的 C + + auto _ ptr。

当您返回 auto _ ptr 时,您是在告诉调用者,您正在向他们提供内存的所有权。

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

这些语义也适用于参数。

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

它可以在大多数风格的 Linux (包括 Android)和 Darwin 上使用。

如果您用来为您的程序编写单元测试,那么您应该养成在测试中系统地运行 val弓形测试的习惯。它可以在早期阶段避免许多内存泄漏。在一个完整的软件中,通常在简单的测试中更容易找到它们。

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

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

这个问题通常可以归结为所有权问题。我强烈推荐阅读 Scott Meyers 的《有效的 C + + 系列》和 Andrei Alexandrescu 的《现代 C + + 设计》。

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

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

这是显而易见的,但是一定要输入 之前,在作用域中输入任何代码

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

任何函数只有一个返回值,这样你就可以在那里进行释放,而且永远不会错过它。

否则很容易犯错:

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

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

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

如果您需要打破这些规则中的任何一条,请记录下来。

它完全是关于指针所有权的。

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

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

例如,检入这个 工地[在 C + + 中调试内存分配] 注意: 删除操作符也有类似的技巧:

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

您可以在一些变量中存储文件的名称,重载删除操作符将知道从哪个位置调用该文件。这样就可以跟踪程序中的每个 delete 和 malloc。在内存检查序列的最后,你应该能够报告哪些分配的内存块没有被“删除”,通过文件名和行号来标识它,我猜这就是你想要的。

你也可以在 Visual Studio 下尝试一些类似 界限检查器的东西,这很有趣,也很容易使用。

呸,你们这些年轻人和你们的新式垃圾收集工。

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

create a thing
use that thing
destroy that thing

有时候在不同的地方创造和毁灭是必要的,我认为很难避免这一点。

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

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

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

我们将所有的分配函数封装在一个图层中,该图层在前面附加一个简短的字符串,在末尾附加一个哨兵标志。例如,您可以调用“ myalloc (pszSomString,iSize,iAlign) ; 或者 new (“ description”,iSize) MyObject () ; 它在内部分配指定的大小加上足够的空间来放置头文件和前哨文件。当然,对于非调试构建,不要忘记注释掉它!做到这一点需要更多的内存,但好处远远大于成本。

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

已经有很多关于如何避免泄漏的方法,但是如果您需要一个工具来帮助您跟踪泄漏,那么可以看一下:

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

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

这是一个非常好的免费网站。

使用 拉尔

  • 忘记垃圾收集 (改为使用 RAII)。请注意,甚至垃圾收集器也可能泄漏(如果你忘记在 Java/C # 中“ null”一些引用) ,垃圾收集器不会帮助你释放资源(如果你有一个对象获得了一个文件的句柄,文件将不会被自动释放时,对象将超出范围,如果你不手动做它在 Java 中,或使用“处理”模式在 C #)。
  • 忘掉“每个函数返回一个”规则。这是一个很好的避免泄漏的 C 建议,但是在 C + + 中它已经过时了,因为它使用了异常(改用 RAII)。
  • 虽然 “三明治模式”是一个很好的 C 建议,但是由于使用了异常,因此它是 在 C + + 中已经过时了(使用 RAII 代替)。

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

学习如何使用智能指针,无论是来自升级、 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.
}

关于 拉尔

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

  • 一旦构造了对象,它就正常工作了! 在构造函数中获取资源。
  • 对象破坏就足够了! 在析构函数中释放资源。
  • 全靠望远镜!作用域对象(参见上面的 doRAIIstatic 示例)将在其声明时构造,并且将在执行退出作用域时被销毁,无论退出方式如何(返回、中断、异常等)。

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

作为一个开发人员,这确实是非常强大的,因为您不需要关心手动资源处理(如在 C 中所做的,或者对于一些在 Java 中密集使用 try/finally的对象) ..。

编辑(2012-02-12)

“范围内的物体... ... 将被摧毁... ... 无论出口是什么”这并不完全正确。有办法欺骗 RAII。任何终止()的味道都会绕过清理。在这方面,EXIT (EXIT _ SUCCESS)是一个矛盾修饰法。

Wilhelmtell

Wilhelmtell 关于这一点非常正确: 有 很特别欺骗 RAII 的方法,所有这些都会导致进程突然停止。

这些都是 很特别的方式,因为 C + + 代码没有随处可见终止、退出等,或者在有例外的情况下,我们确实希望一个 未处理的异常崩溃进程和内核转储其内存映像,而不是在清理之后。

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

(谁会用随意的 C + + 代码调用 terminateexit?... 我记得在使用 暴饮暴食的时候必须处理这个问题: 这个库是非常面向 C 的,甚至主动地设计它使得 C + + 开发人员难以处理,比如不关心 堆栈分配的数据堆栈分配的数据,或者对 从来没有从他们的主循环返回做出“有趣”的决定... 我不会对此发表评论)

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

别写了

Object* x = new Object;

甚至

shared_ptr<Object> x(new Object);

当你能写作的时候

Object x;

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

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

这里用 thread 函数代替

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

很简单,不是吗?如果线程创建失败,auto _ ptr 将释放(删除)资源,否则所有权将传递给线程。 如果线程非常快,以至于在创建之后在

param.release();

在 main 函数/方法中被调用? 什么也没有! 因为我们将“告诉”auto _ ptr 忽略释放。 C + + 的内存管理很简单,不是吗? 干杯,

艾玛!

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

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

然后,当使用 VS2003或更高版本进行调试时,程序退出时(它跟踪 new/delete)会告诉您任何泄漏。这是最基本的,但在过去对我很有帮助。

好问题!

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

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

  • 固定大小的分配器是重点讨论,无处不在的网络

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

  • 一个巨大的进步(源代码分发)可以在游戏编程 Gem 7(2008)中的一篇令人惊奇的文章中找到,这篇文章名为“高性能堆分配器”,作者是 Dimitar Lazarov

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

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

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

让函数拥有对象所有权的情况成为特例。在发生这种情况的所有情况下,一定要在头文件中函数旁边写一条注释,指出这一点。您应该努力确保在大多数情况下,分配对象的模块或类也负责释放对象。

在某些情况下,使用 const 会有很大帮助。如果函数不修改对象,并且不存储对该对象的引用,且该引用在返回后仍然存在,则接受常量引用。从读取调用方代码可以明显看出,函数没有接受对象的所有权。您可以让同一个函数接受一个非常量指针,调用方可能假设被调用方接受所有权,也可能假设被调用方不接受所有权,但是对于常量引用,这是毫无疑问的。

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

我不同意建议参考计数指针的评论。这通常可以很好地工作,但是当您有一个 bug 并且它不工作时,尤其是当您的析构函数执行一些非平凡的操作时,比如在多线程程序中。如果不是太难的话,一定要调整你的设计,不需要参考计数。

按重要性排序的提示:

- 技巧 # 1永远记得声明你的析构函数是“虚拟的”。

- 贴士 # 2使用 RAII

- 技巧 # 3使用升级的智能指针

技巧 # 4不要编写你自己的智能指针错误,使用升级(对于我现在正在做的一个项目,我不能使用升级,而且我不得不调试我自己的智能指针,我肯定不会再走同样的路线,但是现在我不能给我们的依赖性添加升级)

技巧 # 5如果有一些随意的/非性能的关键工作(比如在有数千个物体的游戏中) ,看看 Thorsten Ottosen 的升压指针容器

- 技巧 # 6为您选择的平台找到一个泄漏检测标头,例如 Visual Leak Discovery 的“ vld”标头

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

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

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

要做的第一件事是在任何可能的时候在堆栈上进行分配。这处理了大多数需要为某种目的分配单个对象的情况。

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

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

我与 STL 的不同之处在于,STL 非常关注 Value 对象,而在大多数应用程序中,对象是唯一的实体,在这些容器中使用它们不需要有意义的复制语义。

  • 尽量避免动态分配对象。只要类有合适的构造函数和析构函数,使用类类型的变量,而不是指向它的指针,就可以避免动态分配和释放,因为编译器会替你做这些事情。
    实际上,这也是“智能指针”使用的机制,其他一些作者称之为 RAII; ——)。
  • 将对象传递给其他函数时,应优先选择引用参数而不是指针。这样可以避免一些可能的错误。
  • 尽可能声明参数 const,特别是指向对象的指针。这样对象就不能被“意外地”释放(除非你抛弃了常量; ——))。
  • 尽量减少程序中进行内存分配和释放的位置。例如,如果多次分配或释放同一类型,则为其编写一个函数(或工厂方法; ——)。
    这样,如果需要,可以很容易地创建调试输出(分配和释放地址)。
  • 使用工厂函数从单个函数分配几个相关类的对象。
  • 如果您的类具有带有虚析构函数的公共基类,则可以使用相同的函数(或静态方法)释放所有这些类。
  • 使用 purify 等工具检查您的程序(不幸的是,许多 $/something/...)。