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

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

General guidelines to avoid memory leaks in C++

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


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

不要写

1
Object* x = new Object;

甚至

1
shared_ptr<Object> x(new Object);

当你可以写的时候

1
Object x;

使用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代码的比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

"scoped objects ... will be destructed ... no matter the exit" that's not entirely true. there are ways to cheat RAII. any flavour of terminate() will bypass cleanup. exit(EXIT_SUCCESS) is an oxymoron in this regard.

– wilhelmtell

威廉(Wilhelmtell)对此是完全正确的:有多种欺骗RAII的特殊方法,所有这些方法都会导致过程突然停止。

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

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

(谁在随意的C ++代码中调用terminateexit?...我记得在使用GLUT时必须处理该问题:该库非常面向C,主动设计它会使事情变得困难。对于C ++开发人员而言,他们喜欢不在乎堆栈分配的数据,或者对永不从主循环返回有"有趣的"决定……我不会对此发表评论。


与其尝试手动管理内存,不如尝试使用智能指针。
看一下Boost lib,TR1和智能指针。
智能指针现在也已成为C ++标准(称为C ++ 11)的一部分。


您将要看一下智能指针,例如boost的智能指针。

代替

1
2
3
4
5
6
int main()
{
    Object* obj = new Object();
    //...
    delete obj;
}

当引用计数为零时,boost :: shared_ptr将自动删除:

1
2
3
4
5
6
int main()
{
    boost::shared_ptr<Object> obj(new Object());
    //...
    // destructor destroys when reference count is zero
}

请注意我的最后一条记录,"当引用计数为零时,这是最酷的部分。因此,如果对象有多个用户,则不必跟踪对象是否仍在使用。一旦没有人引用您的对象,共享指针,它将被销毁。

但是,这不是万能药。尽管您可以访问基本指针,但是除非您对它的作用充满信心,否则您不希望将其传递给第三方API。很多时候,将您的"发布"内容发送到其他线程以完成创建范围之后要完成的工作。这与Win32中的PostThreadMessage很常见:

1
2
3
4
5
6
7
8
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向量和map容器实现的,但有一些区别:

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

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


ah,你们年幼的孩子和您的新型垃圾收集器...

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

1
2
3
create a thing
use that thing
destroy that thing

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

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

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

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


好问题!

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

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

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

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

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

  • 可以在本文中找到大量资源

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


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

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


已经有很多关于如何不泄漏的信息,但是如果您需要一个工具来帮助您跟踪泄漏,请查看以下内容:

  • VS下的BoundsChecker
  • FluidStudio的MMGR C / C ++库
    http://www.paulnettle.com/pub/FluidStudios/MemoryManagers/Fluid_Studios_Memory_Manager.zip(它会覆盖分配方法并创建有关分配,泄漏等的报告)

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


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

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

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

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


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


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


重要性提示:

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

-提示#2使用RAII

-提示#3使用boost的smartpointer

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

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

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


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

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

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

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

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


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

1
2
3
4
5
allocate
if allocation succeeded:
{ //scope)
     deallocate()
}

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


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

  • 我创建了对象(可能是通过调用分配新对象的函数间接创建的),然后使用了它(或调用的函数使用了它),然后释放了它。
  • 有人给了我参考,所以我不应该释放它。
  • 如果您需要违反任何这些规则,请记录下来。

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


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

    1
    2
    3
    #ifdef _DEBUG
    #define new DEBUG_NEW
    #endif

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


    • 尝试避免动态分配对象。只要类具有适当的构造函数和析构函数,就可以使用类类型的变量,而不是指向它的指针,并且可以避免动态分配和释放,因为编译器会为您执行此操作。
      实际上,这也是"智能指针"所使用的机制,并且被其他一些作者称为RAII ;-)。

    • 当您将对象传递给其他函数时,与指针相比,首选引用参数。这样可以避免一些可能的错误。

    • 尽可能声明参数const,尤其是指向对象的指针。这样,对象就无法"意外地"释放(除非您将const移开;-))。

    • 最小化程序中用于进行内存分配和释放的位置数。例如如果确实多次分配或释放相同类型,请为其编写一个函数(或工厂方法;-)。
      这样,您可以根据需要轻松地创建调试输出(分配和释放地址,...)。

    • 使用工厂函数可以从单个函数分配多个相关类的对象。

    • 如果您的类具有带虚拟析构函数的通用基类,则可以使用相同的函数(或静态方法)释放所有它们。

    • 使用purify之类的工具检查程序(不幸的是许多$ /€/ ...)。


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

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

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

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


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

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


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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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();
    ...

    这里是线程函数

    1
    2
    3
    4
    5
    6
    7
    8
    extern"C" void* th_func(void* p) {
       try {
           std::auto_ptr<myparams> param((myparams*)p);
           ...
       } catch(...) {
       }
       return 0;
    }

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

    1
    param.release();

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

    埃玛!


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

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

    例如,在此站点中检查[在C ++中调试内存分配]
    注意:删除运算符有一个技巧,例如:

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

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

    您还可以尝试在Visual Studio下使用BoundsChecker之类的工具,它非常有趣且易于使用。


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

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


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


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


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

    否则容易出错:

    1
    2
    3
    4
    5
    new a()
    if (Bad()) {delete a; return;}
    new b()
    if (Bad()) {delete a; delete b; return;}
    ... // etc.

    推荐阅读