Como evitar vazamentos de memória ao usar um vetor de ponteiros para alocar dinamicamente objetos em C ++?
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?
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_ptr
S 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:
- Você está tendo um vetor como o vetor <base*>
- Você está empurrando os ponteiros para este vetor depois de alocar os objetos na pilha
- Você deseja fazer um push_back do ponteiro derivado* neste vetor.
Seguindo as coisas vêm à minha mente:
- O vetor não liberará a memória do objeto apontado pelo ponteiro. Você tem que excluí -lo.
- Nada específico para o vetor, mas o destruidor da classe base deve ser virtual.
- 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.