避免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 }
请注意我的最后一条记录,"当引用计数为零时,这是最酷的部分。因此,如果对象有多个用户,则不必跟踪对象是否仍在使用中。共享指针,它将被销毁。
但是,这不是万能药。尽管我们可以访问基本指针,但是除非我们对它的作用充满信心,否则我们不希望将其传递给第三方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]参数归调用方所有,如果保留引用,被调用方必须进行复制;等等)
回答
与其手动管理内存,不如尝试使用智能指针。
看一下Boost lib,TR1和智能指针。
智能指针现在也已成为C ++标准(称为C ++ 11)的一部分。
回答
阅读RAII并确保我们了解它。
回答
如果可以,请使用boost shared_ptr和标准C ++ auto_ptr。那些传达所有权语义。
当我们返回一个auto_ptr时,我们就是在告诉调用者我们正在给他们内存的所有权。
当我们返回shared_ptr时,我们是在告诉调用者我们对其具有引用,并且它们是所有权的一部分,但这不完全是他们的责任。
这些语义也适用于参数。如果呼叫者将auto_ptr传递给我们,则将为我们提供所有权。
回答
valgrind也是一个很好的工具,可以在运行时检查程序的内存泄漏。
大多数Linux版本(包括Android)和Darwin都可以使用它。
如果用于编写程序的单元测试,则应养成在测试上系统地运行valgrind的习惯。它将有可能在早期避免许多内存泄漏。通常,在完整软件的简单测试中更容易查明它们。
当然,此建议对于任何其他内存检查工具仍然有效。
回答
RAII是一种在C ++中的内存管理中很流行的技术。基本上,我们使用构造函数/析构函数来处理资源分配。当然,由于异常安全性,C ++中还有其他令人讨厌的细节,但是基本思想很简单。
问题通常归结为所有权之一。我强烈建议阅读Scott Meyers撰写的Effective C ++系列和Andrei Alexandrescu撰写的Modern C ++ Design。
回答
如果我们不能/不使用智能指针进行某些操作(尽管这应该是一个巨大的危险信号),请使用以下命令键入代码:
allocate if allocation succeeded: { //scope) deallocate() }
这很明显,但是请确保在键入范围中的任何代码之前先键入它
回答
另外,如果有标准库类(例如向量),请不要使用手动分配的内存。确保如果违反该规则,则我们具有虚拟析构函数。
回答
从任何函数返回一个正好。这样一来,我们就可以在那里进行释放,而永远不会错过它。
否则容易出错:
new a() if (Bad()) {delete a; return;} new b() if (Bad()) {delete a; delete b; return;} ... // etc.
回答
如果要手动管理内存,则有两种情况:
- 我创建了对象(可能是通过调用分配新对象的函数间接创建的),然后使用了它(或者调用的函数使用了它),然后释放了它。
- 有人给了我参考,所以我不应该释放它。
如果我们需要违反任何这些规则,请记录下来。
这全部与指针所有权有关。
回答
我们可以截取内存分配函数,并查看是否有一些内存区域在程序退出时未释放(尽管它并不适合所有应用程序)。
也可以在编译时通过替换new和delete运算符以及其他内存分配函数来完成此操作。
例如,在此站点中检查[在C ++中调试内存分配]
注意:有一个删除操作符的技巧,也是这样的:
#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete #define delete DEBUG_DELETE
我们可以在一些变量中存储文件的名称,以及重载的delete操作符何时知道从哪个位置调用文件。这样,我们可以跟踪程序中每个删除和malloc的情况。在内存检查序列的最后,我们应该能够报告未"删除"哪些已分配的内存块,并通过文件名和行号进行标识,这正是我们想要的。
我们也可以在Visual Studio中尝试类似BoundsChecker的方法,这很有趣并且易于使用。
回答
ah,你年幼的孩子和你新成立的垃圾收集器...
关于"所有权"的非常严格的规则是,软件的哪个对象或者部分有权删除该对象。清除注释和明智的变量名称,以使其在指针"拥有"或者"只是看起来,不要触摸"时变得明显。为了帮助确定谁拥有什么,请在每个子例程或者方法中尽可能遵循"三明治"模式。
create a thing use that thing destroy that thing
有时有必要在千差万别的地方创造和破坏;我想避免这种情况。
在任何需要复杂数据结构的程序中,我都会使用"所有者"指针创建一个包含其他对象的严格的清晰对象树。该树对应用程序域概念的基本层次结构进行建模。例如,一个3D场景拥有对象,灯光,纹理。程序退出时,在渲染结束时,有一种清除所有内容的清晰方法。
每当一个实体需要访问另一个实体,进行扫描或者其他任何操作时,就会根据需要定义许多其他指针。这些就是"随便看"。对于3D场景示例,对象使用纹理但不拥有纹理。其他对象可能使用相同的纹理。对象的破坏不会导致任何纹理的破坏。
是的,这很耗时,但这就是我要做的。我很少遇到内存泄漏或者其他问题。但是后来我在高性能科学,数据采集和图形软件的有限领域工作。我不经常进行诸如银行和电子商务,事件驱动的GUI或者高度网络化的异步混乱之类的交易。也许新的方式在这里有优势!
回答
我们将所有分配函数包装在一层,该层在前面添加一个简短的字符串,在末尾添加一个哨兵标志。因此,例如,我们将调用" myalloc(pszSomeString,iSize,iAlignment);或者new(" description",iSize)MyObject();这将在内部分配指定的大小以及足够的空间用于标题和标记。 ,不要忘记将其注释为非调试版本!这样做需要更多的内存,但是好处远远超过了成本。
首先,它具有三个好处:通过快速搜索在某些"区域"中分配的代码,而在这些区域应该释放时不进行清理,可以轻松快速地跟踪泄漏的代码。通过检查以确保所有标记均完好无损来检测边界何时被覆盖也很有用。在寻找那些隐秘的崩溃或者阵列失误时,这为我们节省了很多时间。第三个好处是跟踪内存的使用情况,以查看谁是大型播放器,它们是MemDump中某些描述的整理,例如,当"声音"占用的空间超出预期时,告诉我们。
回答
已经有很多关于如何不泄漏的信息,但是如果我们需要一个工具来跟踪泄漏,请查看以下内容:
- VS下的BoundsChecker
- 来自FluidStudio的MMGR C / C ++库http://www.paulnettle.com/pub/FluidStudios/MemoryManagers/Fluid_Studios_Memory_Manager.zip(它覆盖分配方法并创建分配,泄漏等报告)
回答
C ++是为RAII设计的。我认为,实际上没有更好的方法来管理C ++中的内存。
但是请注意不要在本地范围内分配很大的块(例如缓冲区对象)。这可能会导致堆栈溢出,并且如果在使用该块时在边界检查方面存在缺陷,则可以覆盖其他变量或者返回地址,从而导致各种安全漏洞。
回答
其他人则提到了避免内存泄漏的方法(例如智能指针)。但是,一旦有了内存配置文件和内存分析工具,它们通常是跟踪内存问题的唯一方法。
Valgrind memcheck是一款出色的免费软件。
回答
使用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代码的比较:
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 ++代码中调用terminate
或者exit
?...我记得在使用GLUT时必须处理该问题:该库非常面向C,积极设计它以使事情变得困难。对于C ++开发人员来说,他们喜欢不在乎堆栈分配的数据,或者对永不从其主循环返回做出"有趣的"决定……对此我将不予评论)。
回答
我完全赞同关于RAII和智能指针的所有建议,但是我还想添加一个更高层次的技巧:最容易管理的内存是我们从未分配的内存。与Cand Java之类的语言(几乎所有内容都是引用)不同,在C ++中,我们应尽可能将对象放在堆栈中。正如我看到的一些人(包括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(); ...
这里是线程函数
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 ++内存管理容易吗?
干杯,
埃玛!
回答
仅对于MSVC,将以下内容添加到每个.cpp文件的顶部:
#ifdef _DEBUG #define new DEBUG_NEW #endif
然后,在使用VS2003或者更高版本进行调试时,程序退出时(程序跟踪新/删除),系统会告知我们任何泄漏。这是基本的,但过去对我有帮助。
回答
好问题!
如果我们使用的是c ++,并且正在开发实时CPU和内存绑定应用程序(例如游戏),则需要编写自己的内存管理器。
我认为我们可以做的更好的是合并各个作者的一些有趣的作品,我可以给我们一些提示:
- 固定大小分配器在网上无处不在
- 小对象分配由Alexandrescu于2001年在他的完美著作"现代c ++设计"中提出。
- 可以从Dimitar Lazarov撰写的《 Game Programming Gem 7》(2008年)中名为" High Performance Heap allocator"的高性能文章中找到惊人的进步(已分发源代码)。
- 可以在本文中找到大量资源
不要自己开始编写noob没用的分配器。
回答
这些错误的一个常见来源是当我们拥有一种方法,该方法接受对象的引用或者指针,但所有权不清楚。样式和注释约定可以减少这种可能性。
让函数获得对象所有权的情况为特例。在所有发生这种情况的情况下,请确保在头文件中的函数旁边写一个注释来表明这一点。我们应该努力确保在大多数情况下,分配对象的模块或者类也负责取消分配该对象。
在某些情况下,使用const会很有帮助。如果函数不会修改对象,并且不存储对该对象的引用(在返回后仍然存在),请接受const引用。通过阅读调用者的代码,很明显函数尚未接受该对象的所有权。我们可能具有相同的功能来接受非const指针,并且调用方可能会也可能不会假定被调用方已接受所有权,但是使用const引用就没有问题。
不要在参数列表中使用非常量引用。读取呼叫者代码时,还不清楚被呼叫者可能保留了对该参数的引用。
我不同意建议引用计数指针的意见。这通常可以正常工作,但是当我们遇到错误并且不起作用时,尤其是在析构函数执行不重要的操作时(例如在多线程程序中)。如果不太难的话,一定要尝试调整设计,使其不需要引用计数。
回答
重要性提示:
-提示#1始终记得将析构函数声明为"虚拟"。
-提示#2使用RAII
-提示#3使用boost的smartpointer
-提示#4不要编写自己的越野车Smartpointer,使用boost(在我现在正在进行的项目中,我无法使用boost,而且我不得不调试自己的智能指针,我一定不会再次使用相同的路线,但是现在又不能再增加我们的依赖项了)
-Tip#5如果它对某些临时性/非性能性至关重要(例如在具有数千个对象的游戏中),请查看Thorsten Ottosen的boost指针容器
-Tip#6查找所选平台的泄漏检测头,例如Visual Leak Detection的" vld"头
回答
以与管理其他资源(句柄,文件,数据库连接,套接字...)相同的方式管理内存。 GC也不会。
回答
valgrind(仅适用于* nix平台)是一个非常不错的内存检查器
回答
大多数内存泄漏是由于不清楚对象所有权和生存期而导致的。
首先要做的是在可能的情况下在堆栈上进行分配。在大多数情况下,我们需要出于某些目的分配单个对象,这可以解决这一问题。
如果确实需要"新建"一个对象,则在大多数情况下,它在整个生命周期中都只有一个明显的所有者。对于这种情况,我倾向于使用一堆收集模板,这些收集模板旨在通过指针"拥有"存储在其中的对象。它们是使用STL向量和map容器实现的,但有一些区别:
- 这些集合不能复制或者分配给它们。 (一旦它们包含对象。)
- 指向对象的指针插入其中。
- 删除集合后,将首先在集合中的所有对象上调用析构函数。 (我有另一个版本,该版本断言是否被破坏且不为空。)
- 由于它们存储指针,因此我们也可以在这些容器中存储继承的对象。
我对STL的看法是,它专注于Value对象,而在大多数应用程序中,对象是唯一的实体,不具有在这些容器中使用所需的有意义的复制语义。
回答
尝试避免动态分配对象。只要类具有适当的构造函数和析构函数,请使用类类型的变量,而不是指向它的指针,并且可以避免动态分配和释放,因为编译器会为我们完成此操作。
实际上,这也是"智能指针"所使用的机制,并被其他一些作者称为RAII ;-)。当我们将对象传递给其他函数时,应优先使用引用参数而不是指针。这样可以避免一些可能的错误。
尽可能声明参数const,尤其是指向对象的指针。这样,就不能"意外地"释放对象(除非我们将const移开;-))。最小化程序中用于进行内存分配和释放的位置数。例如如果确实多次分配或者释放相同类型,请为其编写一个函数(或者工厂方法;-)。
这样,我们可以根据需要轻松地创建调试输出(已分配和释放地址,...)。
使用工厂函数可以从单个函数分配多个相关类的对象。如果类具有带虚拟析构函数的通用基类,则可以使用相同的函数(或者静态方法)释放所有它们。
使用purify之类的工具检查程序(不幸的是很多$ // ...)。