有什么一般性质的技巧可以确保我在C++程序中不会泄漏内存? 我如何确定谁应该释放动态分配的内存?
有什么一般性质的技巧可以确保我在C++程序中不会泄漏内存? 我如何确定谁应该释放动态分配的内存?
我完全赞同关于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。任何一种终止()方式都将绕过清理。在这方面,exit(EXIT_SUCCESS)是一个自相矛盾的说法。
威廉·泰尔是正确的:有一些 特殊 的方法可以欺骗 RAII,但这些方法最终都会导致进程突然停止。
这些方法是 特殊 的,因为 C++ 代码并不会充斥着 terminate、exit 等等,或者在异常情况下,我们确实希望 未处理的异常 使进程崩溃,并在清理之前转储其内存映像。
但我们仍然必须了解这些情况,因为虽然它们很少发生,但仍然可能发生。
(谁会在普通的 C++ 代码中调用 terminate
或 exit
呢?……我记得当我玩 GLUT 时,就不得不处理这个问题:这个库非常面向 C,甚至特意设计得让 C++ 开发人员感到困难,比如不关心 堆栈分配数据,或者对于 从主循环中永远不返回 的“有趣”决策……我不想评论这个)。
doRAIIDynamic
而不是 doRAIIStatic
?如果我没记错的话,静态情况下对象是在堆上分配的,所以如果 T
对象很大,你会想要使用动态版本,对吗? - Robert您需要查看智能指针,例如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!
}
像往常一样,在使用任何工具时都要动动脑筋...
啊,你们这些年轻人总是喜欢用新式垃圾回收器...
非常严格的"所有权"规则 - 哪个对象或软件部分有权删除对象。清晰的注释和明智的变量名,以使指针是否"拥有"或"只看不碰"显而易见。为了帮助决定谁拥有什么,在每个子程序或方法中尽可能地遵循"三明治"模式。
create a thing
use that thing
destroy that thing
有时需要在不同的位置创建和销毁对象,难以避免。在任何需要复杂数据结构的程序中,我会创建一个严格清晰、包含其他对象的对象树-使用“owner”指针。这个树形结构模拟应用领域概念的基本层次结构。例如,一个3D场景拥有对象、灯光、纹理。在渲染结束并退出程序时,有明确的方法销毁所有内容。
当一个实体需要访问另一个实体、扫描数组或者其他情况下,我会定义需要的其他指针;这些是“只读”的。对于3D场景的例子来说,一个对象使用了纹理但并不拥有它;其他对象可能也会使用相同的纹理。对象的销毁不会触发任何纹理的销毁。
虽然这很费时间,但我经常这样做。我很少出现内存泄漏或其他问题。但是我工作在高性能科学、数据采集和图形软件的有限领域。我很少处理银行和电子商务交易、事件驱动的GUI或高度网络化的异步混沌。也许新潮的方式在那里有优势!
大多数内存泄漏都是由于不清楚对象所有权和生命周期而导致的。
第一件事情是尽可能在栈上分配。这解决了大多数需要为某些目的分配单个对象的情况。
如果您确实需要使用 'new' 来创建对象,那么在其余的生命周期中,它往往会有一个明显的所有者。对于这种情况,我倾向于使用一堆集合模板,它们被设计用于通过指针拥有其中存储的对象。它们是使用STL vector和map容器实现的,但有一些区别:
我对STL的看法是,它过于专注于值对象,而在大多数应用程序中,对象是独特的实体,它们没有必要在这些容器中使用所需的有意义的复制语义。
很棒的问题!
如果您正在使用C ++并且正在开发实时CPU和内存绑定应用程序(如游戏),则需要编写自己的内存管理器。
我认为您可以将各种作者的一些有趣作品合并起来,我可以给您一些提示:
固定大小的分配器在网络上被广泛讨论
Alexandrescu在他的完美著作“现代C ++设计”中于2001年介绍了小对象分配
在Dimitar Lazarov撰写的Game Programming Gem 7(2008)的一篇惊人文章“高性能堆分配器”中可以找到一个伟大的进步(带有源代码分发)
您可以在此文章中找到资源的伟大列表
不要开始编写无用的新手分配器... 首先要进行文档化。
在C++中,已经变得越来越流行的一种内存管理技术是RAII。通常利用构造函数和析构函数来处理资源分配。 当然,在C++中有一些其他令人讨厌的细节,由于异常安全性,但基本思想还是相当简单的。
问题通常归结为所有权问题。我强烈建议阅读Scott Meyers的Effective C++系列和Andrei Alexandrescu的Modern C++ Design。
已经有很多关于如何避免泄漏的资料,但如果您需要一个工具来帮助您跟踪泄漏,请看一下以下内容: