如何在使用C++中的指向动态分配对象的指针向量时避免内存泄漏?

78
我正在使用一个指向对象的指针的向量。这些对象都是从基类派生而来,并且被动态地分配和存储。
例如,我有像这样的代码:
vector<Enemy*> Enemies;

我将从Enemy类派生出一个类,然后动态分配派生类的内存,就像这样:

enemies.push_back(new Monster());

我需要注意哪些事项以避免内存泄漏和其他问题?


也许一个以英语为母语的人可以理解你想要表达的意思,但我却不知所云。首先,您正在谈论内存泄漏 -> 依赖于语言/平台; 我想你是指C++。避免内存泄漏已经被广泛讨论过了(http://stackoverflow.com/search?q=c%2B%2B+raii)。您需要一个虚析构函数才能正确删除从基类型中进行操作。 - gimpf
1
“vectors to pointers”是什么意思?你是指“指针的向量”吗? - Tamás Szelei
是的,我正在使用 C++。是的,我确实指的是指针向量。抱歉我的英语不好。 - akif
17
好的,我会尽力以最简洁明了的语言翻译,并确保不改变原意。以下是需要翻译的内容:"I took a shot at rewording it all, please edit or comment if I've removed any information, or if it's not clear." - GManNickG
你只需要删除向量中定义的新类的指针的每个元素。当向量容器超出作用域时,它本身将自动被释放。请注意,如果您的继承层次结构是虚拟的,则需要显式定义析构函数,因为这也可能会导致内存泄漏。 - Owl
4个回答

165

std :: vector 会像往常一样为您管理内存,但这些内存将是指针,而不是对象。

这意味着,一旦您的向量超出作用域,您的类将在内存中丢失。例如:

#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<base*> container;

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(new derived());

} // leaks here! frees the pointers, doesn't delete them (nor should it)

int main()
{
    foo();
}

您需要做的是确保在向量超出作用域之前删除所有对象:

#include <algorithm>
#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<base*> container;

template <typename T>
void delete_pointed_to(T* const ptr)
{
    delete ptr;
}

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(new derived());

    // free memory
    std::for_each(c.begin(), c.end(), delete_pointed_to<base>);
}

int main()
{
    foo();
}

这很难维护,因为我们必须记得执行某些操作。更重要的是,如果在分配元素和释放循环之间发生异常,释放循环将永远不会运行,你仍然会遇到内存泄漏!这被称为异常安全,这是自动执行释放操作的关键原因。
更好的方法是指针自己删除。这些被称为智能指针,标准库提供了std::unique_ptrstd::shared_ptrstd::unique_ptr表示对某些资源的唯一(未共享、单所有者)指针。这应该是您默认的智能指针,并且完全替代任何原始指针使用。
auto myresource = /*std::*/make_unique<derived>(); // won't leak, frees itself

std::make_unique在C++11标准中由于疏忽被遗漏了,但是你可以自己创建一个。如果你能使用make_unique,不建议直接创建unique_ptr,可以按照以下方式进行:

std::unique_ptr<derived> myresource(new derived());

唯一指针仅具有移动语义;它们不能被复制:

auto x = myresource; // error, cannot copy
auto y = std::move(myresource); // okay, now myresource is empty

这就是我们在容器中使用它所需的全部内容:

#include <memory>
#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<std::unique_ptr<base>> container;

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(make_unique<derived>());

} // all automatically freed here

int main()
{
    foo();
}

shared_ptr 具有引用计数的复制语义,它允许多个所有者共享对象。它跟踪对象存在多少个 shared_ptr,当最后一个 shared_ptr 停止存在(计数变为零)时,它释放指针。复制只是增加引用计数(移动则以更低的成本转移所有权)。您可以使用 std::make_shared(或直接如上所示,但由于 shared_ptr 必须内部进行分配,因此通常更高效且在技术上更安全的做法是使用 make_shared)来创建它们。

#include <memory>
#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<std::shared_ptr<base>> container;

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(std::make_shared<derived>());

} // all automatically freed here

int main()
{
    foo();
}

记住,通常情况下你应该使用std::unique_ptr作为默认选项,因为它更轻量级。此外,std::shared_ptr可以由std::unique_ptr构建(但反之不行),所以从小处开始也没关系。
或者,您可以使用一个用于存储指向对象的指针的容器,例如boost::ptr_container
#include <boost/ptr_container/ptr_vector.hpp>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

// hold pointers, specially
typedef boost::ptr_vector<base> container;

void foo()
{
    container c;

    for (int i = 0; i < 100; ++i)
        c.push_back(new Derived());

} // all automatically freed here

int main()
{
    foo();
}

虽然在C++03中,boost::ptr_vector<T>有明显的用途,但我无法确定它现在是否还具有相关性,因为我们可以使用std::vector<std::unique_ptr<T>>,可能几乎没有可比较的开销,但这种说法应该经过测试。

无论如何,在你的代码中永远不要明确释放资源。封装起来以确保自动处理资源管理。你的代码中不应该有任何拥有原始指针。

作为游戏中的默认设置,我可能会选择std::vector<std::shared_ptr<T>>。我们预计会进行共享,直到分析表明需要改变时,它足够快速、安全且易于使用。


2
如果他实际上正在编写游戏代码(正如示例所暗示的那样),那么引用计数指针(或者boost如何实现共享指针)很可能过于昂贵。恒定的内存占用量(特别是对于AI对象)比移除用于释放内存的for循环更为重要,这是一个更高的设计目标。 - Dan O
5
@Dan:不管怎样,您都将不得不进行清理,如果速度太慢,问题不在于如何处理,而在于如何避免首次进行清理。如果您无法绕开它,请首先使用最干净的方式进行清理,然后进行测量,仅在之后尝试改进。Boost意味着数千对敏锐眼睛正在改进代码。这很难被超越:我已经看到boost的 shared_ptr 在CPU/GPU密集型3D应用程序中胜过使用特殊目的分配器的自定义智能指针。在您进行测量之前,您永远不会知道... - sbi
哈,不,我是指我的评论在下面有关于我对于编辑你的信息不确定的看法。如果你的信息是我的答案,那么我的评论就不是问题了。网络连接不好。 :P 无论如何,感谢你的赞美。 - GManNickG
1
@sbi 我并不是在提倡使用不同的shared_ptr,而是在提倡使用不同的内存管理方法。在游戏代码情况下,共享指针很可能不合适。事实上,它们对于原帖提交的示例来说完全不合适。我的大部分论点都在这里概括了:http://www.bureau14.fr/blogea/2009/08/smart-pointers-are-overused/ - Dan O
这个答案被其他人引用,但现在有点过时了。值得修订一下,提到std::unique_ptr而不是std::auto_ptrboost::shared_ptr - beldaz
显示剩余9条评论

10
使用vector<T*>的问题在于,每当向量意外地超出范围(例如抛出异常时),向量会为您清理,但这只会释放用于保存指针的内存,而不是您为指针引用的内容分配的内存。因此,GMan的delete_pointed_to函数的价值有限,因为它仅在没有错误发生时起作用。
你需要做的是使用智能指针:
vector< std::tr1::shared_ptr<Enemy> > Enemies;

(如果您的标准库没有TR1,请使用boost :: shared_ptr 。) 除了非常罕见的角落情况(循环引用)外,这只是消除了对象生命周期的麻烦。

编辑:请注意,GMan在他详细的答案中也提到了这一点。


1
@GMan:我完全阅读了你的回答并看到了这个。我本来只会提到delete_pointer_to的可能性,而不详细说明它,因为它太低效了。我觉得有必要把现成的解决方案放入一个简短、简单的“这样做”的答案中。 (Boost的指针容器是一个不错的选择,虽然我确实给了它们一个赞。)如果你感到被误读了,我很抱歉。 - sbi
2
我认为你的观点非常好。我应该编辑进去吗?我总是不确定这一点。如果我编辑我的答案使其更完整,我感觉自己在“窃取”其他人的声望。 - GManNickG
4
@GMan:请继续改善排在首位的答案。你的回答很好,详细,绝对值得排在首位。不要在意声望积分,如果有一个少做这种事情的程序员,那比任何声望积分都更有帮助。:) - sbi
也许这会帮助其他人,从而节省他人的时间 :) - akif
2
我的天啊!友好合作的讨论,更不用说在在线讨论中达成协议了?这完全是闻所未闻的!干得好 :) - e.James

9
我假设以下内容:
  1. 您拥有一个类似于vector< base* >的向量
  2. 在堆上分配对象后,您将指针推入此向量中
  3. 您想将derived*指针push_back到此向量中。
以下是我想到的内容:
  1. 向量不会释放指针指向的对象的内存。您必须自己删除它。
  2. 与向量无关,但基类析构函数应该是虚拟的。
  3. vector< base* >和vector< derived* >是两种完全不同的类型。

你的假设完全正确。很抱歉,我没有能够恰当地解释清楚。还有其他需要帮忙的吗? - akif
1
如果可能的话,避免使用原始指针,并使用GMan所描述的方法。 - Naveen

-1
非常小心的一件事是:如果有两个内容完全相同的派生对象Monster(),并且你想从你的向量(基类指向派生Monster对象的指针)中删除重复的Monster对象。如果你使用了用于删除重复项的标准习语(sort、unique、erase:参见链接#2),你将遇到内存泄漏问题和/或重复删除问题,可能导致分段错误(我个人在LINUX机器上看到过这些问题)。
std::unique() 的问题在于,在向量末尾的[duplicatePosition,end)范围的重复项是未定义的。可能发生的情况是,那些未定义的(?) 项可能是额外的副本或缺失的副本。
问题在于,std::unique() 不适合正确处理指针向量。原因是std::unique从向量的末尾“向下”复制独特项,对于普通对象的向量,这会调用COPY CTOR,如果COPY CTOR编写正确,则不会出现内存泄漏问题。但当它是一个指针向量时,除了“位拷贝”之外,没有COPY CTOR,因此仅简单地复制指针。

除了使用智能指针之外,还有其他方法可以解决这些内存泄漏问题。一种方法是编写自己略微修改过的std::unique()版本,称为“your_company::unique()”。基本技巧是,不是复制元素,而是交换两个元素。并且您必须确保不是比较两个指针,而是调用BinaryPredicate跟随两个指针到对象本身,并比较这两个“Monster”派生对象的内容。

1)@SEE_ALSO: http://www.cplusplus.com/reference/algorithm/unique/

2)@SEE_ALSO: 什么是消除重复项和对向量进行排序的最有效方法?

第二个链接写得非常好,适用于std::vector,但存在内存泄漏、重复释放(有时会导致段错误)的问题。

3)@SEE_ALSO: valgrind(1)。这个LINUX上的“内存泄漏”工具非常神奇!我强烈建议使用它!

我希望在未来的文章中发布一个漂亮的版本 "my_company::unique()"。目前,它还不完美,因为我想让具有BinaryPredicate的3个参数版本无缝地适用于函数指针或FUNCTOR,并且我在处理两者时遇到了一些问题。如果我无法解决这些问题,我将发布我所拥有的,并让社区尝试改进我已经完成的部分。


这似乎完全没有回答问题。如果你所关心的只是可能存在多个指针指向同一个对象,那么你应该使用引用计数智能指针,例如 boost::smart_ptr - beldaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接