【问题标题】:How to avoid memory leaks when using a vector of pointers to dynamically allocated objects in C++?在 C++ 中使用指向动态分配对象的指针向量时如何避免内存泄漏?
【发布时间】:2010-11-24 13:43:37
【问题描述】:

我正在使用指向对象的指针向量。这些对象派生自一个基类,并且是动态分配和存储的。

例如,我有类似的东西:

vector<Enemy*> Enemies;

我将从 Enemy 类派生,然后为派生类动态分配内存,如下所示:

enemies.push_back(new Monster());

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

【问题讨论】:

  • 也许一个以英语为母语的人可以理解你想说的话,但我迷路了。首先,您在谈论内存泄漏-> 语言/平台相关;我希望你的意思是 C++。避免内存泄漏已经被广泛讨论(stackoverflow.com/search?q=c%2B%2B+raii)。您需要一个虚拟析构函数来从基本类型中删除才能正常工作。
  • “指向指针的向量”是什么意思?你的意思是“向量指针”?
  • 是的,我正在使用 C++。是的,我的意思是指针向量。对不起我的英语不好
  • 我尝试重新措辞,如果我删除了任何信息,或者不清楚,请编辑或评论。
  • 只需要删除向量中定义的新类的指针向量的每个元素。向量容器本身会在超出范围时自动释放。请注意,如果您的继承层次结构是虚拟的,那么您需要显式定义您的析构函数,因为这也可能导致内存泄漏。

标签: c++ stl pointers vector derived


【解决方案1】:

需要非常小心的一点是,如果有两个 Monster() DERIVED 对象,其内容的值相同。假设您想从向量中删除 DUPLICATE Monster 对象(指向 DERIVED Monster 对象的 BASE 类指针)。如果您使用标准习语来删除重复项(排序、唯一、擦除:参见 LINK #2],您将遇到内存泄漏问题和/或重复删除问题,可能导致 SEGMENTATION VOIOLATIONS(我个人在LINUX机器)。

std::unique() 的问题是向量末尾的 [duplicatePosition,end) 范围 [inclusive,exclusive) 中的重复项未定义为 ?。可能发生的情况是那些未定义的 ((?) 项可能是多余的重复项或缺少的重复项。

问题在于 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: What's the most efficient way to erase duplicates and sort a vector?

第二个链接写得很好,适用于 std::vector,但存在内存泄漏、重复释放(有时导致 SEGMENTATION 违规)对于 std::vector

3) @SEE_ALSO: valgrind(1)。 LINUX 上的这个“内存泄漏”工具的发现令人惊叹!我强烈推荐使用它!

我希望在以后的帖子中发布一个不错的“my_company::unique()”版本。现在,它并不完美,因为我希望具有 BinaryPredicate 的 3-arg 版本可以无缝地用于函数指针或 FUNCTOR,并且我在正确处理两者时遇到了一些问题。如果我无法解决这些问题,我会发布我所拥有的,并让社区尝试改进我目前所做的工作。

【讨论】:

  • 这似乎根本没有回答这个问题。如果您只关心指向同一对象的多个指针的可能性,您应该只使用引用计数的智能指针,例如boost::smart_ptr
【解决方案2】:

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_ptr

std::unique_ptr 表示指向某个资源的唯一(非共享、单一所有者)指针。这应该是您的默认智能指针,并且可以完全替代任何原始指针的使用。

auto myresource = /*std::*/make_unique<derived>(); // won't leak, frees itself

std::make_unique 因疏忽而在 C++11 标准中缺失,但您可以自己制作一个。要直接创建unique_ptr(如果可以,不推荐超过make_unique),请执行以下操作:

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_ptrs,当最后一个不存在时(该计数变为零),它释放指针。复制只是增加了引用计数(并以更低、几乎免费的成本转移所有权)。您可以使用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();
}

虽然 boost::ptr_vector&lt;T&gt; 在 C++03 中有明显的用途,但我现在无法谈论相关性,因为我们可以使用 std::vector&lt;std::unique_ptr&lt;T&gt;&gt; 可能几乎没有可比开销,但应该测试这种说法。

无论如何,永远不要在你的代码中明确地释放东西。总结一下以确保自动处理资源管理。您的代码中不应包含原始的拥有指针。

作为游戏的默认设置,我可能会选择std::vector&lt;std::shared_ptr&lt;T&gt;&gt;。无论如何,我们希望共享,它足够快,直到配置文件另有说明,它是安全的,并且易于使用。

【讨论】:

  • 如果他实际上是在编写游戏代码(正如示例所暗示的那样),那么一个 ref 计数指针(或者无论如何实现共享指针的 boost)可能过于昂贵.. 一个恒定的内存占用(尤其是对于 AI objects) 是一个比删除 for 循环来解除分配更高的设计目标。
  • b/w Pointer Contains 和 Shared Pointers 我应该选择哪一个?为什么?
  • @Dan:您将不得不以某种方式进行清理,如果这太慢,问题不在于采用哪种方式,而是如何避免一开始就必须这样做地方。如果绕不开,先用最干净的方法,然后测量,然后再尝试改进。 Boost 意味着数千双敏锐的眼睛在改进代码。难以超越:我已经看到 boost 的 shared_ptr 在 CPU/GPU 密集型 3D 应用程序中使用专用分配器优于自定义智能指针。在你测量之前,你永远不会知道......
  • 更新了我的答案。幸运的是,这次我们的“答案”匹配了,sbi。 :P(个人资料!)
  • @sbi 我不是在提倡不同的 shared_ptr,而是在提倡不同的内存管理方法。在游戏代码案例中,共享指针很可能是不合适的。事实上,它们完全不适合原始海报提交的示例。我的大部分论点总结在这里:bureau14.fr/blogea/2009/08/smart-pointers-are-overused
【解决方案3】:

使用vector&lt;T*&gt; 的问题在于,每当向量意外超出范围时(例如抛出异常时),向量会自行清理,但这只会释放它管理的用于保存 pointer,而不是您为指针所指内容分配的内存。所以GMan's delete_pointed_to function 的价值是有限的,因为它只有在没有问题的情况下才有效。

你需要做的是使用智能指针:

vector< std::tr1::shared_ptr<Enemy> > Enemies;

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

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

【讨论】:

  • @GMan:我完全阅读了您的答案并看到了这一点。我只会提到delete_pointer_to 的可能性而没有详细说明,因为它太逊色了。我觉得有必要将现成的解决方案放入一个简短、简单的“这样做”的答案中。 (不过,Boost 的指针容器是一个不错的选择,我确实为提及它们投了赞成票。)如果您误读了,我很抱歉。
  • 我认为你的观点非常好,实际上。我应该编辑它吗?在这一点上我总是不确定。如果我编辑我的答案以使其更完整,我觉得我正在“窃取”其他人的代表。
  • @GMan:继续改进堆栈顶部的答案。你的回答很好而且很详细,definitley 应该在那里。见鬼了,如果做这种事情的程序员少了一个,那将比任何代表点对我们所有人的帮助都大得多。 :)
  • 也许将来会帮助别人,从而节省别人的时间:)
  • 我的话!友好合作的话语,更不用说协议在网上讨论?完全闻所未闻!干得好:)
【解决方案4】:

我假设如下:

  1. 你有一个像 vector
  2. 这样的向量
  3. 在堆上分配对象后,您将指针推送到此向量
  4. 您希望将派生* 指针的 push_back 推入此向量。

我想到了以下事情:

  1. Vector 不会释放指针指向的对象的内存。你必须自己删除它。
  2. 没有特定于向量的东西,但基类析构函数应该是虚拟的。
  3. vector 和vector 是两种完全不同的类型。

【讨论】:

  • 你的假设是绝对正确的。抱歉,我无法正确解释。还有什么吗?
  • 尽可能避免使用原始指针,并使用 GMan 的回答中描述的方法。
猜你喜欢
  • 2011-03-27
  • 2013-04-06
  • 1970-01-01
  • 2019-04-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-31
  • 2014-08-13
相关资源
最近更新 更多