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


68

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

例如,我有类似的东西:

vector<Enemy*> Enemies;

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

enemies.push_back(new Monster());

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


也许说英语的人可以解读您想说的话,但我迷路了。首先,您正在谈论内存泄漏->语言/平台相关性;我希望您的意思是C ++。避免内存泄漏已被广泛讨论(stackoverflow.com/search?q=c%2B%2B+raii)。您需要一个虚拟析构函数来从基本类型中删除才能正常工作。
gimpf

1
“指针向量”是什么意思?你的意思是“矢量指针”?
陶Szelei

是的,我正在使用C ++。是的,我的意思是指针向量。对不起,我的英文不好
阿基夫

16
我对所有内容都改了字,如果删除了任何信息或不清楚,请编辑或评论。
GManNickG

只是您需要删除指向向量中定义的新类的指针向量的每个元素。超出范围时,向量容器本身将自动释放。请注意,如果继承层次结构是虚拟的,则需要显式定义析构函数,因为这也可能导致内存泄漏。
猫头鹰

Answers:


150

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_uniqueC ++ 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_ptr一个对象存在多少个s,并且当最后一个不存在时(该计数变为零),它释放指针。复制只是增加了参考计数(并且以较低的几乎免费的成本转移所有权)。您可以使用它们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<T>在C ++ 03中有明显的用途,但我现在不敢说相关性,因为我们可以使用std::vector<std::unique_ptr<T>>几乎没有甚至没有可比的开销,但是应该对此声明进行测试。

无论如何,永远不要在代码中显式地释放东西。整理一下内容以确保自动处理资源管理。您的代码中应该没有原始的拥有指针。

作为游戏的默认设置,我可能会选择std::vector<std::shared_ptr<T>>。无论如何,我们都希望共享,它的速度足够快,直到性能分析另有说明为止,它是安全的,并且易于使用。


2
如果他实际上是在编写游戏代码(如所暗示的示例),则引用计数的指针(或通过boost实现共享指针)可能过于昂贵。.恒定的内存占用量(尤其是AI对象)比删除for循环以解除分配。
丹奥

我应该选择b / w指针包含和共享指针中的哪一个,为什么?
09年

5
@Dan:您将必须进行某种方式的清理,如果这样做太慢,问题不在于采用哪种方式,而是首先要避免这样做。如果无法解决问题,请先使用最干净的方法,然后再进行测量,然后再尝试进行改进。Boost意味着数千双敏锐的眼睛在改进代码。难以克服的是:我已经看到boostshared_ptr在CPU / GPU密集型3D应用程序中使用专用分配器的性能优于自定义智能指针。在您测量之前,您永远不会知道...
2009年

更新了我的答案。幸运的是,我们的“答案”这次匹配了,sbi。:P(个人资料!)
GManNickG

1
@sbi我不提倡使用其他shared_ptr,而是提倡采用不同的内存管理方法。在游戏代码的情况下,共享指针很可能是不合适的。实际上,他们完全不适合原始海报提交的示例。我的大部分论点总结如下:Bureau14.fr/blogea/2009/08/smart-pointers-are-overed
Dan O

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的指针容器是一个不错的选择,我确实提到了它们。)如果您感到误读,我感到抱歉。
2009年

2
实际上,我认为您的观点很好。我应该编辑它吗?我现在一直不确定。如果我编辑答案以使其更完整,那么我感觉就像是在“偷”别人的代表。
GManNickG

3
@GMan:继续改进最重要的答案。您的答案很好而且很详细,definitley应该在那里。对于销售代表来说,如果没有那么多的程序员来做这种事情,那对我们来说将比任何销售代表要多得多。:)
2009年

也许将来会帮助别人,从而节省别人的时间:)
akif

2
我的话!友好合作的话语,更何况协议中的在线讨论?完全闻所未闻!
做得好

9

我假设以下内容:

  1. 您有一个像vector <base *>这样的向量
  2. 您在堆上分配对象后将指针推到此向量
  3. 您想在此向量中执行派生*指针的push_back。

我想到以下事情:

  1. 向量不会释放指针所指向的对象的内存。您必须删除它本身。
  2. 对于vector没有任何特定要求,但是基类的析构函数应该是虚的。
  3. vector <base *>和vector <派生*>是两种完全不同的类型。

您的假设是绝对正确的。抱歉,我无法正确解释。还有别的事吗?
09年

1
如果可能,避免使用原始指针,并使用GMan答案中描述的方法。
Naveen

-1

要特别小心的一件事是,如果有两个内容相同的Monster()派生对象。假设您想从向量中删除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: 擦除重复项和对向量进行排序的最有效方法是什么?

第二个链接写得很好,并且可以用于std :: vector,但存在内存泄漏,std :: vector的重复释放(有时会导致SEGMENTATION违规)

3)@SEE_ALSO:valgrind(1)。LINUX上的“内存泄漏”工具在其发现的功能上令人赞叹!我强烈建议使用它!

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


这似乎根本无法回答问题。如果您担心的是可能存在多个指向同一对象的指针,则应仅使用引用计数的智能指针,例如boost::smart_ptr
beldaz
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.