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

aki*_*kif 66 c++ pointers stl vector derived

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

例如,我有类似的东西:

vector<Enemy*> Enemies;
Run Code Online (Sandbox Code Playgroud)

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

enemies.push_back(new Monster());
Run Code Online (Sandbox Code Playgroud)

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

GMa*_*ckG 145

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();
}
Run Code Online (Sandbox Code Playgroud)

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

#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();
}
Run Code Online (Sandbox Code Playgroud)

但这很难维护,因为我们必须记住执行某些操作.更重要的是,如果在元素分配和释放循环之间发生异常,则释放循环将永远不会运行,并且您仍然会遇到内存泄漏!这称为异常安全,这是解除分配需要自动完成的关键原因.

如果指针删除自己会更好.这些被称为智能指针,标准库提供std::unique_ptrstd::shared_ptr.

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

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

std::make_unique由于监督而缺少C++ 11标准,但您可以自己制作一个.要直接创建unique_ptr(make_unique如果可以的话,不推荐使用),请执行以下操作:

std::unique_ptr<derived> myresource(new derived());
Run Code Online (Sandbox Code Playgroud)

唯一指针只有移动语义; 他们无法复制:

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

这就是我们需要在容器中使用它:

#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();
}
Run Code Online (Sandbox Code Playgroud)

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();
}
Run Code Online (Sandbox Code Playgroud)

请记住,您通常希望将其std::unique_ptr用作默认值,因为它更轻量级.另外,std::shared_ptr可以用a构建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();
}
Run Code Online (Sandbox Code Playgroud)

虽然boost::ptr_vector<T>在C++ 03中有明显的用途,但我现在不能谈论相关性,因为我们可以使用std::vector<std::unique_ptr<T>>很少甚至没有可比较的开销,但是这个声明应该被测试.

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

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

  • @Dan:你必须做某种方式进行清理,如果这样做太慢,问题不在于采用哪种方式,而是如何避免必须首先进行清理.如果你不能绕过它,先用最干净的方法,然后测量,然后再尝试改进.Boost意味着数千双敏锐的眼睛改善了代码.很难理解:我已经看到boost的`shared_ptr`在CPU/GPU密集型3D应用程序中使用专用分配器优于自定义智能指针.在你衡量之前,你永远不会知道...... (5认同)
  • 如果他实际上正在编写游戏代码(作为示例类型的暗示),那么引用计数指针(或者实现共享指针的提升)可能过于昂贵.恒定的内存占用(特别是对于AI对象)是一个更高的设计目标.删除for循环以取消分配. (2认同)

Nav*_*een 9

我假设如下:

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

以下事情浮现在我的脑海中:

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


sbi*_*sbi 9

使用的麻烦vector<T*>在于,每当向量意外地超出范围时(比如抛出异常),向量会自行清理,但这只会释放它为保存指针而管理的内存,而不是你分配的内存指针指的是什么.因此,GMan的delete_pointed_to功能价值有限,因为它只在没有出错的情况下才有效.

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

vector< std::tr1::shared_ptr<Enemy> > Enemies;
Run Code Online (Sandbox Code Playgroud)

(如果你的std lib没有TR1,请使用boost::shared_ptr.)除了非常罕见的极端情况(循环引用)之外,这简单地消除了对象生存期的麻烦.

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

  • @GMan:继续前进并改进堆叠顶部的答案.你的答案是好的和详细的,definitley应该在那里.对于代表来说,如果那里有一个较少的程序员在做这种事情,这将比任何代表点都更能帮助我们.`:)` (3认同)
  • 实际上,我认为你的观点非常好.我应该编辑吗?在这一点上我总是不确定.如果我编辑我的答案所以它更完整,我觉得我"偷"了其他人的代表. (2认同)
  • 我的话!友好合作的话语,更不用说*协议*在线讨论?完全闻所未闻!干得好:) (2认同)