Como evitar vazamentos de memória ao usar um vetor de ponteiros para alocar dinamicamente objetos em C ++?

StackOverflow https://stackoverflow.com/questions/1361139

Pergunta

Estou usando um vetor de ponteiros para objetos. Esses objetos são derivados de uma classe base e estão sendo alocados e armazenados dinamicamente.

Por exemplo, eu tenho algo como:

vector<Enemy*> Enemies;

E vou derivar da classe inimiga e depois alocar dinamicamente a memória da classe derivada, assim:

enemies.push_back(new Monster());

Quais são as coisas que preciso estar ciente para evitar vazamentos de memória e outros problemas?

Foi útil?

Solução

std::vector Gerenciará a memória para você, como sempre, mas essa memória será de ponteiros, não objetos.

O que isso significa é que suas aulas serão perdidas na memória quando seu vetor sair do escopo. Por exemplo:

#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();
}

O que você precisa fazer é garantir que você exclua todos os objetos antes que o vetor saia do escopo:

#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();
}

Isso é difícil de manter, porém, porque temos que lembrar de realizar alguma ação. Mais importante, se uma exceção ocorreu entre a alocação de elementos e o loop de desalocação, o loop de desalocação nunca funcionaria e você ficará preso ao vazamento de memória de qualquer maneira! Isso é chamado de segurança de exceção e é uma razão crítica para que a desalocação precisa ser feita automaticamente.

Melhor seria se os ponteiros se excitassem. Teses são chamados de indicadores inteligentes, e a biblioteca padrão fornece std::unique_ptr e std::shared_ptr.

std::unique_ptr Representa um ponteiro único (não compartilhado, de um único) para algum recurso. Este deve ser o seu ponteiro inteligente padrão e a substituição geral total de qualquer uso de ponteiro bruto.

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

std::make_unique está faltando no padrão C ++ 11 por supervisão, mas você pode fazer um você mesmo. Para criar diretamente um unique_ptr (não recomendado sobre make_unique se puder), faça isso:

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

Ponteiros únicos têm apenas a semântica de movimentos; Eles não podem ser copiados:

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

E isso é tudo o que precisamos para usá -lo em um contêiner:

#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 tem semântica de cópia de contagem de referência; Ele permite que vários proprietários compartilhem o objeto. Ele rastreia quantos shared_ptrS existe para um objeto e, quando o último deixa de existir (essa contagem vai para zero), ele libera o ponteiro. A cópia simplesmente aumenta a contagem de referência (e a movimentação de transferências a um custo mais baixo, quase livre). Você os faz com std::make_shared (ou diretamente como mostrado acima, mas porque shared_ptr tem que fazer alocações internamente, geralmente é mais eficiente e tecnicamente mais seguro de exceção usar 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();
}

Lembre -se, você geralmente quer usar std::unique_ptr Como padrão, porque é mais leve. Adicionalmente, std::shared_ptr pode ser construído a partir de um std::unique_ptr (Mas não vice -versa), por isso não há problema em começar pequeno.

Como alternativa, você pode usar um recipiente criado para armazenar ponteiros para objetos, como um 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();
}

Enquanto boost::ptr_vector<T> Tinha uso óbvio em C ++ 03, não posso falar da relevância agora porque podemos usar std::vector<std::unique_ptr<T>> com provavelmente pouca ou nenhuma sobrecarga comparável, mas essa reivindicação deve ser testada.

Sem considerar, nunca explicitamente livre coisas em seu código. Entenda as coisas para garantir que o gerenciamento de recursos seja tratado automaticamente. Você não deve ter indicadores de propriedade crua em seu código.

Como padrão em um jogo, eu provavelmente iria com std::vector<std::shared_ptr<T>>. Esperamos compartilhar de qualquer maneira, é rápido o suficiente até que o perfil diga o contrário, é seguro e é fácil de usar.

Outras dicas

Estou assumindo o seguinte:

  1. Você está tendo um vetor como o vetor <base*>
  2. Você está empurrando os ponteiros para este vetor depois de alocar os objetos na pilha
  3. Você deseja fazer um push_back do ponteiro derivado* neste vetor.

Seguindo as coisas vêm à minha mente:

  1. O vetor não liberará a memória do objeto apontado pelo ponteiro. Você tem que excluí -lo.
  2. Nada específico para o vetor, mas o destruidor da classe base deve ser virtual.
  3. vetor <base*> e vetor <derivado*> são dois tipos totalmente diferentes.

O problema de usar vector<T*> é que, sempre que o vetor sai do escopo inesperadamente (como quando uma exceção é lançada), o vetor se limpa atrás de si, mas isso só liberará a memória que ele consegue manter o ponteiro, não a memória que você alocou para o que os ponteiros estão se referindo. Então Gman's delete_pointed_to função é de valor limitado, pois só funciona quando nada dá errado.

O que você precisa fazer é usar um ponteiro inteligente:

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

(Se sua DST lib vier sem TR1, use boost::shared_ptr em vez disso.) Exceto por casos de canto muito raros (referências circulares), isso simplesmente remove o problema da vida útil do objeto.

Editar: Observe que Gman, em sua resposta detalhada, menciona isso também.

Uma coisa a ter muito cuidado é se há dois objetos derivados de monstro () cujo conteúdo é idêntico em valor. Suponha que você deseja remover os objetos de monstro duplicados do seu vetor (ponteiros da classe base para objetos de monstro derivados). Se você usou o idioma padrão para remover duplicatas (classificar, exclusivo, apagar: veja o link #2], você encontrará problemas de vazamento de memória e/ou duplicar problemas de exclusão, possivelmente levando a segmentação de vozes (eu vi pessoalmente esses problemas em Máquina Linux).

O problema com o std :: exclusivo () é que as duplicatas na faixa [duplicatePosition, final) [inclusive, exclusivas) no final do vetor são indefinidas como? O que pode acontecer é que esses itens indefinidos (?) Podem ser duplicados extra ou uma duplicata ausente.

O problema é que o std :: Único () não é voltado para lidar com um vetor de ponteiros corretamente. O motivo é que as cópias exclusivas do STD :: exclusivas do final do vetor "abaixam" no início do vetor. Para um vetor de objetos simples, isso chama a cópia e, se o ctor for gravado corretamente, não haverá problema de vazamentos de memória. Mas quando é um vetor de ponteiros, não há cópia além de "copiar bit" e, portanto, o ponteiro em si é simplesmente copiado.

Existem maneiras de resolver esse vazamento de memória além de usar um ponteiro inteligente. Uma maneira de escrever sua própria versão ligeiramente modificada do std :: Único () como "your_company :: Único ()". O truque básico é que, em vez de copiar um elemento, você trocaria dois elementos. E você teria que ter certeza de que, em vez de comparar dois ponteiros, você chama BinaryPredicate que segue os dois ponteiros com o objeto e compara o conteúdo desses dois objetos derivados de "monstro".

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

2) @see_also: Qual é a maneira mais eficiente de apagar duplicatas e classificar um vetor?

O 2º link é escrito excelentemente e funcionará para um vetor de std ::, mas tem vazamentos de memória, duplicar libera (às vezes resultando em violações de segmentação) para um std :: vetor

3) @SEE_ALSO: Valgrind (1). Esta ferramenta de "vazamento de memória" no Linux é incrível no que pode encontrar! Eu recomendo usá -lo!

Espero postar uma boa versão de "my_company :: Único ()" em uma postagem futura. No momento, não é perfeito, porque eu quero que a versão de 3-Arg tenha o BinaryPredicate funcione perfeitamente para um ponteiro de função ou um functor, e estou tendo alguns problemas para lidar com os dois corretamente. Se eu não puder resolver esses problemas, postarei o que tenho e deixarei a comunidade melhorar o que fiz até agora.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top