有什么常规提示可确保我不会泄漏C ++程序中的内存?我如何确定谁应该释放已经动态分配的内存?
有什么常规提示可确保我不会泄漏C ++程序中的内存?我如何确定谁应该释放已经动态分配的内存?
Answers:
与其尝试手动管理内存,不如尝试使用智能指针。
看一下Boost lib,TR1和智能指针。
智能指针现在也已成为C ++标准(称为C ++ 11)的一部分。
我完全赞同关于RAII和智能指针的所有建议,但是我还想添加一个更高层次的技巧:最容易管理的内存是您从未分配的内存。与C#和Java这样的语言(几乎所有内容都是引用)不同,在C ++中,应尽可能将对象放在堆栈中。正如我看到的一些人(包括Stroustrup博士)所指出的那样,垃圾收集从未在C ++中流行的主要原因是,编写良好的C ++首先不会产生大量垃圾。
不要写
Object* x = new Object;
甚至
shared_ptr<Object> x(new Object);
当你可以写的时候
Object x;
这篇文章似乎是重复的,但是在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.
}
概括起来(在Ogre Psalm33的评论之后),RAII依赖于三个概念:
这意味着在正确的C ++代码中,大多数对象将不会使用构造new
,而是会在堆栈上声明。对于那些使用构造的对象new
,所有内容都将以某种方式进行范围调整(例如,附加到智能指针)。
作为开发人员,这确实非常强大,因为您无需关心手动资源处理(如在C中完成的操作,或者对于Java中某些需要大量使用try
/的对象finally
)。
“范围内的对象……将被破坏……无论出口如何”都不是完全正确的。有很多方法可以欺骗RAII。任何类型的terate()都会绕过清理。在这方面,exit(EXIT_SUCCESS)是矛盾的。
– 威廉姆特
威廉姆勒(Wilhelmtell)对此非常正确:欺骗RAII的方法非常特殊,所有方法都会导致过程突然停止。
这些是特殊的方式,因为C ++代码不会被终止,退出等乱七八糟,或者在有异常的情况下,我们确实希望有一个未处理的异常使进程崩溃并使内核按原样转储内存映像,而不是在清除之后。
但是我们仍然必须知道这些情况,因为尽管它们很少发生,但仍然可能发生。
(谁调用terminate
或exit
使用临时C ++代码?...我记得在使用GLUT时必须处理该问题:该库非常面向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!
}
与往常一样,将您的思维能力与任何工具结合使用...
大多数内存泄漏是由于不清楚对象所有权和生存期而导致的。
首先要做的是在可能的情况下在堆栈上进行分配。在大多数情况下,您需要出于某种目的分配一个对象,这可以解决这个问题。
如果确实需要“新建”一个对象,则在大多数情况下,它在整个生命周期中都只有一个明显的所有者。对于这种情况,我倾向于使用一堆收集模板,这些收集模板设计用于通过指针“拥有”存储在其中的对象。它们是通过STL向量和map容器实现的,但有一些区别:
我对STL的看法是,它非常关注Value对象,而在大多数应用程序中,对象是唯一的实体,不具有在这些容器中使用所需的有意义的复制语义。
ah,你年幼的孩子和你新成立的垃圾收集器...
关于“所有权”的非常严格的规则-哪些对象或软件的一部分有权删除该对象。清除注释和明智的变量名称,以使其在指针“拥有”或“只是看起来,不要触摸”时变得明显。为了帮助确定谁拥有什么,请在每个子例程或方法中尽可能遵循“三明治”模式。
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。
已经有很多关于如何不泄漏的信息,但是如果您需要一个工具来帮助您跟踪泄漏,请查看:
在整个项目中共享并了解内存所有权规则。使用COM规则可实现最佳一致性([in]参数归调用方所有,被调用方必须复制; [out]参数归调用方所有,如果保留引用,被调用方必须进行复制;等等)
如果您不能/不使用智能指针进行某些操作(尽管应该是一个巨大的危险信号),请使用以下命令输入代码:
allocate
if allocation succeeded:
{ //scope)
deallocate()
}
这很明显,但是请确保在键入作用域中的任何代码之前先键入它
这些错误的一个常见来源是当您拥有一种方法,该方法接受对象的引用或指针,但所有权不清楚。样式和注释约定可以减少这种可能性。
让函数拥有对象所有权的情况为特例。在所有发生这种情况的情况下,请确保在头文件中的函数旁边写一个注释以表明这一点。您应努力确保在大多数情况下,分配对象的模块或类也负责取消分配该对象。
在某些情况下,使用const会很有帮助。如果函数不会修改对象,并且不存储对该对象的引用(在返回后仍然存在),请接受const引用。通过阅读调用者的代码,很明显您的函数尚未接受该对象的所有权。您可能具有相同的函数来接受非const指针,并且调用方可能会也可能不会假定被调用方已接受所有权,但是使用const引用就没有问题。
不要在参数列表中使用非常量引用。读取调用者代码时,还不清楚被调用者可能保留了对该参数的引用。
我不同意建议引用计数指针的意见。这通常可以正常工作,但是当您遇到错误并且不起作用时,尤其是在析构函数执行不重要的操作(例如在多线程程序中)的情况下。如果不太难的话,一定要尝试调整您的设计,使其不需要引用计数。
重要性提示:
-提示#1始终记得将析构函数声明为“虚拟”。
-提示#2使用RAII
-提示#3使用boost的smartpointer
-提示#4不要编写自己的越野车Smartpointers,使用boost(在我现在正在进行的项目中,我无法使用boost,而且我不得不调试自己的智能指针,我一定不会再次使用相同的路线,但是现在又不能再增加依赖关系了)
-Tip#5如果它对休闲/非性能至关重要(例如在具有数千个对象的游戏中),请查看Thorsten Ottosen的boost指针容器
-Tip#6查找所选平台的泄漏检测头,例如Visual Leak Detection的“ vld”头
如果可以,请使用boost shared_ptr和标准C ++ auto_ptr。这些传达所有权语义。
当您返回auto_ptr时,您就是在告诉调用者您正在给他们内存的所有权。
当您返回shared_ptr时,您是在告诉调用者您对其具有引用,并且它们属于所有权,但这不完全是他们的责任。
这些语义也适用于参数。如果呼叫者将auto_ptr传递给您,则将为您提供所有权。
其他人则提到了首先避免内存泄漏的方法(例如智能指针)。但是,一旦有了内存配置文件和内存分析工具,它们通常是跟踪内存问题的唯一方法。
Valgrind memcheck是一款出色的免费软件。
您可以拦截内存分配函数,并查看是否有一些内存区域在程序退出时未释放(尽管它并不适合所有应用程序)。
也可以在编译时通过替换new和delete运算符以及其他内存分配函数来完成此操作。
例如,在此站点 [在C ++中调试内存分配]中检查。
#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中某些描述的整理可以告诉您“声音”何时比您预期的占用更多的空间。
关于在不同位置分配和销毁的唯一示例之一是线程创建(您传递的参数)。但是即使在这种情况下也很容易。这是创建线程的函数/方法:
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也不会帮助您。