¿Cómo evitar memory leaks cuando se utiliza un vector de punteros a objetos dinámicamente asignados en C ++?

Estoy usando un vector de punteros a los objetos. Estos objetos se derivan de una clase base y se están asignando y almacenando dinámicamente.

Por ejemplo, tengo algo como:

vector Enemies; 

y derivaré de la clase Enemy y luego asignaré dinámicamente la memoria para la clase derivada, así:

 enemies.push_back(new Monster()); 

¿Qué cosas debo tener en cuenta para evitar memory leaks y otros problemas?

std::vector administrará la memoria para usted, como siempre, pero esta memoria será de punteros, no de objetos.

Lo que esto significa es que sus clases se perderán en la memoria una vez que su vector se salga del scope. Por ejemplo:

 #include  struct base { virtual ~base() {} }; struct derived : base {}; typedef std::vector 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(); } 

Lo que tendría que hacer es asegurarse de eliminar todos los objetos antes de que el vector salga del scope:

 #include  #include  struct base { virtual ~base() {} }; struct derived : base {}; typedef std::vector container; template  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); } int main() { foo(); } 

Sin embargo, esto es difícil de mantener porque tenemos que recordar realizar alguna acción. Más importante aún, si se produjera una excepción entre la asignación de elementos y el bucle de desasignación, el bucle de desasignación nunca se ejecutaría y, de todos modos, ¡se quedaría atascado con la pérdida de memoria! Esto se llama seguridad de excepción y es una razón crítica por la cual la desasignación debe hacerse automáticamente.

Sería mejor si los punteros se borraran a sí mismos. Estas tesis se denominan punteros inteligentes, y la biblioteca estándar proporciona std::unique_ptr y std::shared_ptr .

std::unique_ptr representa un std::unique_ptr único (no compartido, de un solo propietario) para algún recurso. Este debería ser su puntero inteligente predeterminado y el reemplazo total completo de cualquier uso de puntero sin formato.

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

std::make_unique falta en el estándar C ++ 11 por supervisión, pero puede crear uno usted mismo. Para crear directamente un unique_ptr (no recomendado sobre make_unique si puedes), haz esto:

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

Punteros únicos tienen semántica de movimiento solamente; ellos no pueden ser copiados:

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

Y esto es todo lo que necesitamos para usarlo en un contenedor:

 #include  #include  struct base { virtual ~base() {} }; struct derived : base {}; typedef std::vector> container; void foo() { container c; for (unsigned i = 0; i < 100; ++i) c.push_back(make_unique()); } // all automatically freed here int main() { foo(); } 

shared_ptr tiene semántica de copia de recuento de referencias; permite que múltiples propietarios compartan el objeto. Realiza un seguimiento de cuántos shared_ptr s existen para un objeto, y cuando el último deja de existir (ese conteo va a cero), libera el puntero. Copiar simplemente aumenta el recuento de referencias (y mover la propiedad de las transferencias a un costo menor, casi gratuito). Los haces con std::make_shared (o directamente como se muestra arriba, pero debido a que shared_ptr tiene que hacer asignaciones internamente, generalmente es más eficiente y técnicamente más seguro de excepciones para usar make_shared ).

 #include  #include  struct base { virtual ~base() {} }; struct derived : base {}; typedef std::vector> container; void foo() { container c; for (unsigned i = 0; i < 100; ++i) c.push_back(std::make_shared()); } // all automatically freed here int main() { foo(); } 

Recuerde, generalmente desea usar std::unique_ptr como valor predeterminado porque es más liviano. Además, std::shared_ptr se puede construir a partir de std::unique_ptr (pero no al revés), por lo que está bien comenzar de a poco.

Alternativamente, puede usar un contenedor creado para almacenar punteros a objetos, como boost::ptr_container :

 #include  struct base { virtual ~base() {} }; struct derived : base {}; // hold pointers, specially typedef boost::ptr_vector container; void foo() { container c; for (int i = 0; i < 100; ++i) c.push_back(new Derived()); } // all automatically freed here int main() { foo(); } 

Aunque boost::ptr_vector tuvo un uso obvio en C ++ 03, no puedo hablar de la relevancia ahora porque podemos usar std::vector> con probablemente poca o ninguna sobrecarga comparable , pero esta afirmación debe ser probada.

Independientemente, nunca explícitamente cosas gratis en tu código . Concluya las cosas para asegurarse de que la gestión de los recursos se realice de forma automática. No deberías tener punteros propietarios en tu código.

Como predeterminado en un juego, probablemente vaya con std::vector> . Esperamos compartirlo de todos modos, es lo suficientemente rápido hasta que los perfiles indiquen lo contrario, es seguro y fácil de usar.

Estoy asumiendo lo siguiente:

  1. Estás teniendo un vector como vector
  2. Está presionando los punteros a este vector después de asignar los objetos en montón
  3. Desea hacer un push_back del puntero derivado * en este vector.

Las siguientes cosas vienen a mi mente:

  1. Vector no liberará la memoria del objeto apuntado por el puntero. Tienes que eliminarlo solo.
  2. Nada específico para el vector, pero el destructor de la clase base debe ser virtual.
  3. vector y vector son dos tipos totalmente diferentes.

El problema con el uso del vector es que, cada vez que el vector sale inesperadamente del scope (como cuando se lanza una excepción), el vector se limpia después de ti, pero esto solo liberará la memoria que maneja para sostener el puntero , no la memoria que asignó para lo que los punteros se refieren. Así que la función delete_pointed_to de delete_pointed_to tiene un valor limitado, ya que solo funciona cuando nada sale mal.

Lo que debes hacer es usar un puntero inteligente:

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

(Si su std lib viene sin TR1, utilice boost::shared_ptr en boost::shared_ptr lugar.) Excepto en casos de esquina muy raros (referencias circulares), esto simplemente elimina el problema de la duración del objeto.

Editar : Tenga en cuenta que GMan, en su respuesta detallada, menciona esto también.

Una cosa para tener mucho cuidado es SI hay dos objetos DERIVADOS de Monster () cuyo contenido es idéntico en valor. Supongamos que quiere eliminar los objetos DUPLICATE Monster de su vector (indicadores de clase BASE a objetos DERIVED Monster). Si utilizó el modismo estándar para eliminar duplicados (ordenar, único, borrar: vea ENLACE Nº 2), se encontrará con problemas de pérdida de memoria y / o problemas de eliminación de duplicados, lo que posiblemente lleve a VOIOLACIONES DE SEGMENTACIÓN (personalmente he visto estos problemas en Máquina LINUX).

El problema con std :: unique () es que los duplicados en el rango [duplicatePosition, end) [inclusive, exclusivo] al final del vector no están definidos como?. Lo que puede suceder es que esos elementos no definidos ((?) Pueden ser duplicados adicionales o duplicados faltantes.

El problema es que std :: unique () no está diseñado para manejar un vector de punteros correctamente. La razón es que std :: unique copia únicos del final del vector “hacia abajo” hacia el comienzo del vector. Para un vector de objetos simples, esto invoca COPY CTOR, y si COPY CTOR está escrito correctamente, no hay problemas de pérdida de memoria. Pero cuando es un vector de punteros, no hay COPY COPY que no sea “copia bit a bit”, por lo que el puntero mismo se copia simplemente.

Hay maneras de resolver estas memory leaks además de usar un puntero inteligente. Una forma de escribir su propia versión ligeramente modificada de std :: unique () como “your_company :: unique ()”. El truco básico es que en lugar de copiar un elemento, podrías intercambiar dos elementos. Y debe asegurarse de que en lugar de comparar dos punteros, llame a BinaryPredicate que sigue a los dos punteros al objeto en sí, y compare el contenido de esos dos objetos derivados de “Monster”.

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

2) @SEE_ALSO: ¿Cuál es la forma más eficiente de borrar duplicados y ordenar un vector?

El segundo enlace está escrito de forma excelente, y funcionará para un std :: vector pero tiene memory leaks, duplicados libres (a veces resultando en violaciones de SEGMENTACIÓN) para un std :: vector

3) @SEE_ALSO: valgrind (1). ¡Esta herramienta de “pérdida de memoria” en LINUX es increíble en lo que puede encontrar! ¡ALTAMENTE recomiendo usarlo!

Espero publicar una buena versión de “my_company :: unique ()” en una publicación futura. En este momento, no es perfecto, porque quiero que la versión de 3 arg tenga BinaryPredicate para funcionar sin problemas, ya sea para un puntero de función o un FUNCTOR, y estoy teniendo algunos problemas para manejar ambos correctamente. Si no puedo resolver esos problemas, publicaré lo que tengo, y dejaré que la comunidad intente mejorar lo que he hecho hasta ahora.