Pregunta

¿Cuáles son algunos consejos generales para asegurarme de no perder memoria en los programas C++?¿Cómo puedo saber quién debería liberar memoria asignada dinámicamente?

¿Fue útil?

Solución

En lugar de administrar la memoria manualmente, intente utilizar punteros inteligentes cuando corresponda.
Échale un vistazo al Impulsar la biblioteca, TR1, y punteros inteligentes.
Además, los punteros inteligentes ahora forman parte del estándar C++ llamado C++11.

Otros consejos

Apoyo totalmente todos los consejos sobre RAII y los punteros inteligentes, pero también me gustaría agregar un consejo de nivel ligeramente superior:La memoria más fácil de administrar es la memoria que nunca asignaste.A diferencia de lenguajes como C# y Java, donde casi todo es una referencia, en C++ debes poner objetos en la pila siempre que puedas.Como he visto señalar a varias personas (incluido el Dr. Stroustrup), la razón principal por la que la recolección de basura nunca ha sido popular en C++ es que, en primer lugar, un C++ bien escrito no produce mucha basura.

no escribas

Object* x = new Object;

o incluso

shared_ptr<Object> x(new Object);

cuando puedes simplemente escribir

Object x;

Usar RAII

  • Olvídate de la recolección de basura (Utilice RAII en su lugar).Tenga en cuenta que incluso el Recolector de basura también puede tener fugas (si olvida "anular" algunas referencias en Java/C#), y que el Recolector de basura no le ayudará a deshacerse de los recursos (si tiene un objeto que adquirió un identificador para un archivo, el archivo no se liberará automáticamente cuando el objeto salga del alcance si no lo hace manualmente en Java o usa el patrón "eliminar" en C#).
  • Olvídese de la regla de "una devolución por función".Este es un buen consejo de C para evitar fugas, pero está desactualizado en C++ debido al uso de excepciones (use RAII en su lugar).
  • Y mientras el "patrón sándwich" es un buen consejo C, está desactualizado en C ++ debido a su uso de excepciones (use RAII en su lugar).

Esta publicación parece repetitiva, pero en C++, el patrón más básico que debes conocer es RAII.

Aprenda a usar punteros inteligentes, tanto de boost, TR1 o incluso el humilde (pero a menudo bastante eficiente) auto_ptr (pero debe conocer sus limitaciones).

RAII es la base tanto de la seguridad de excepciones como de la eliminación de recursos en C++, y ningún otro patrón (sándwich, etc.) le brindará ambas (y la mayoría de las veces, no le brindará ninguna).

Vea a continuación una comparación del código RAII y no RAII:

void doSandwich()
{
   T * p = new T() ;
   // do something with p
   delete p ; // leak if the p processing throws or return
}

void doRAIIDynamic()
{
   std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

void doRAIIStatic()
{
   T p ;
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

Acerca de RAII

Para resumir (después del comentario de Salmo ogro 33), RAII se basa en tres conceptos:

  • Una vez construido el objeto, ¡simplemente funciona! Adquiera recursos en el constructor.
  • ¡La destrucción de objetos es suficiente! Libera recursos en el destructor.
  • ¡Se trata de alcances! Los objetos con alcance (consulte el ejemplo de doRAIIStatic arriba) se construirán en su declaración y se destruirán en el momento en que la ejecución salga del alcance, sin importar cómo sea la salida (retorno, interrupción, excepción, etc.).

Esto significa que en código C++ correcto, la mayoría de los objetos no se construirán con new, y en su lugar se declarará en la pila.Y para aquellos construidos usando new, todo será de alguna manera alcance (p.ej.adjunto a un puntero inteligente).

Como desarrollador, esto es muy poderoso, ya que no necesitará preocuparse por el manejo manual de recursos (como se hace en C, o para algunos objetos en Java, que hace un uso intensivo de try/finally para ese caso)...

Editar (2012-02-12)

"objetos con alcance...será destruido...no importa la salida" eso no es del todo cierto.Hay maneras de engañar a RAII.cualquier versión de terminate() omitirá la limpieza.exit(EXIT_SUCCESS) es un oxímoron a este respecto.

wilhelmtell

wilhelmtell tiene toda la razón en eso:Hay excepcional formas de engañar a RAII, todas las cuales conducen a una parada abrupta del proceso.

Esos son excepcional maneras porque el código C++ no está plagado de terminaciones, salidas, etc., o en el caso de excepciones, queremos un Excepción no controlada para bloquear el proceso y volcar la imagen de memoria tal como está, y no después de la limpieza.

Pero aun así debemos conocer esos casos porque, aunque rara vez suceden, aún pueden suceder.

(quien llama terminate o exit en código C++ casual?...Recuerdo haber tenido que lidiar con ese problema cuando jugaba con EXCESO:Esta biblioteca está muy orientada a C, llegando incluso a diseñarla activamente para dificultar las cosas a los desarrolladores de C++, como no preocuparse por apilar datos asignados, o tomar decisiones "interesantes" sobre nunca regresando de su bucle principal...No comentaré sobre eso).

Querrá mirar consejos inteligentes, como punteros inteligentes de impulso.

En lugar de

int main()
{ 
    Object* obj = new Object();
    //...
    delete obj;
}

boost::shared_ptr se eliminará automáticamente una vez que el recuento de referencias sea cero:

int main()
{
    boost::shared_ptr<Object> obj(new Object());
    //...
    // destructor destroys when reference count is zero
}

Tenga en cuenta mi última nota, "cuando el recuento de referencias es cero, que es la parte más interesante.Entonces, si tiene varios usuarios de su objeto, no tendrá que realizar un seguimiento de si el objeto todavía está en uso.Una vez que nadie hace referencia a su puntero compartido, se destruye.

Sin embargo, esto no es una panacea.Aunque puede acceder al puntero base, no querrá pasarlo a una API de terceros a menos que esté seguro de lo que está haciendo.Muchas veces, "publicas" cosas en algún otro hilo para que el trabajo se realice DESPUÉS de que finalice el alcance de la creación.Esto es común con PostThreadMessage en Win32:

void foo()
{
   boost::shared_ptr<Object> obj(new Object()); 

   // Simplified here
   PostThreadMessage(...., (LPARAM)ob.get());
   // Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}

Como siempre, utiliza tu capacidad de pensar con cualquier herramienta...

Leer sobre RAII y asegúrate de entenderlo.

La mayoría de las pérdidas de memoria son el resultado de no tener clara la propiedad y la vida útil de los objetos.

Lo primero que debe hacer es realizar asignaciones en la pila siempre que pueda.Esto se ocupa de la mayoría de los casos en los que es necesario asignar un único objeto para algún propósito.

Si necesita "nuevo" un objeto, la mayoría de las veces tendrá un único propietario obvio durante el resto de su vida.Para esta situación, tiendo a usar un montón de plantillas de colecciones que están diseñadas para "poseer" objetos almacenados en ellas mediante puntero.Se implementan con los contenedores de mapas y vectores STL, pero tienen algunas diferencias:

  • Estas colecciones no se pueden copiar ni asignar.(una vez que contengan objetos).
  • En ellos se insertan punteros a objetos.
  • Cuando se elimina la colección, primero se llama al destructor en todos los objetos de la colección.(Tengo otra versión donde afirma si está destruido y no vacío).
  • Dado que almacenan punteros, también puedes almacenar objetos heredados en estos contenedores.

Mi problema con STL es que está tan centrado en los objetos de valor, mientras que en la mayoría de las aplicaciones los objetos son entidades únicas que no tienen una semántica de copia significativa necesaria para su uso en esos contenedores.

Bah, ustedes, jóvenes y sus novedosos recolectores de basura...

Reglas muy estrictas sobre "propiedad": qué objeto o parte del software tiene derecho a eliminar el objeto.Comentarios claros y nombres de variables inteligentes para que quede claro si un puntero "es dueño" o "solo mira, no tocas".Para ayudar a decidir quién posee qué, siga en la medida de lo posible el patrón "sándwich" dentro de cada subrutina o método.

create a thing
use that thing
destroy that thing

A veces es necesario crear y destruir en lugares muy diferentes;Creo que es difícil evitar eso.

En cualquier programa que requiera estructuras de datos complejas, creo un árbol estricto y claro de objetos que contienen otros objetos, utilizando punteros de "propietario".Este árbol modela la jerarquía básica de los conceptos del dominio de aplicación.Ejemplo una escena 3D posee objetos, luces, texturas.Al final del renderizado, cuando el programa se cierra, hay una manera clara de destruir todo.

Muchos otros punteros se definen según sea necesario cada vez que una entidad necesita acceder a otra, para escanear matrices o lo que sea;estos son los "simplemente mirando".Para el ejemplo de la escena 3D: un objeto usa una textura pero no la posee;otros objetos pueden usar esa misma textura.La destrucción de un objeto no no invocar la destrucción de cualquier textura.

Sí, lleva mucho tiempo, pero eso es lo que hago.Rara vez tengo pérdidas de memoria u otros problemas.Pero luego trabajo en el ámbito limitado del software científico, de adquisición de datos y de gráficos de alto rendimiento.No suelo ocuparme de transacciones como en la banca y el comercio electrónico, GUI impulsadas por eventos o caos asincrónico en alta red.¡Quizás los métodos más novedosos tengan allí una ventaja!

¡Gran pregunta!

Si está utilizando C++ y está desarrollando una aplicación de memoria y CPU en tiempo real (como juegos), debe escribir su propio Administrador de memoria.

Creo que lo mejor que puedes hacer es fusionar algunas obras interesantes de varios autores, te puedo dar una pista:

  • El asignador de tamaño fijo es muy discutido en todas partes en la red.

  • Alexandrescu introdujo la asignación de objetos pequeños en 2001 en su libro perfecto "Diseño moderno en c++".

  • Se puede encontrar un gran avance (con código fuente distribuido) en un artículo sorprendente en Game Programming Gem 7 (2008) llamado "Asignador de montón de alto rendimiento" escrito por Dimitar Lazarov.

  • Puede encontrar una gran lista de recursos en este artículo

No empieces a escribir un asignador novato e inútil tú mismo...DOCUMENTATE primero.

Una técnica que se ha vuelto popular en la gestión de memoria en C++ es RAII.Básicamente, utilizas constructores/destructores para manejar la asignación de recursos.Por supuesto, hay algunos otros detalles desagradables en C++ debido a la seguridad de las excepciones, pero la idea básica es bastante simple.

La cuestión generalmente se reduce a la propiedad.Recomiendo encarecidamente leer la serie Effective C++ de Scott Meyers y Modern C++ Design de Andrei Alexandrescu.

Ya hay mucho sobre cómo no tener fugas, pero si necesita una herramienta que le ayude a rastrear las fugas, eche un vistazo a:

¡Usa punteros inteligentes en todas partes que puedas!Clases enteras de pérdidas de memoria simplemente desaparecen.

Comparta y conozca las reglas de propiedad de la memoria en todo su proyecto.El uso de reglas COM logra la mejor coherencia (los parámetros [in] son ​​propiedad de la persona que llama, la persona que llama debe copiarlos;[out] los parámetros son propiedad de la persona que llama, la persona que llama debe hacer una copia si mantiene una referencia;etc.)

valgrind También es una buena herramienta para comprobar las pérdidas de memoria de sus programas en tiempo de ejecución.

Está disponible en la mayoría de las versiones de Linux (incluido Android) y en Darwin.

Si suele escribir pruebas unitarias para sus programas, debería acostumbrarse a ejecutar valgrind sistemáticamente en las pruebas.Potencialmente evitará muchas pérdidas de memoria en una etapa temprana.También suele ser más fácil identificarlos en pruebas sencillas que en un software completo.

Por supuesto, este consejo sigue siendo válido para cualquier otra herramienta de verificación de memoria.

Además, no utilice la memoria asignada manualmente si hay una clase de biblioteca estándar (p. ej.vector).Si viola esa regla, asegúrese de tener un destructor virtual.

Si no puedes/no usas un puntero inteligente para algo (aunque eso debería ser una gran señal de alerta), escribe tu código con:

allocate
if allocation succeeded:
{ //scope)
     deallocate()
}

Eso es obvio, pero asegúrate de escribirlo. antes escribes cualquier código en el alcance

Una fuente frecuente de estos errores es cuando tienes un método que acepta una referencia o un puntero a un objeto pero no deja clara la propiedad.Las convenciones de estilo y comentarios pueden hacer que esto sea menos probable.

Dejemos que el caso en el que la función toma posesión del objeto sea el caso especial.En todas las situaciones en las que esto suceda, asegúrese de escribir un comentario junto a la función en el archivo de encabezado que lo indique.Debe esforzarse por asegurarse de que en la mayoría de los casos el módulo o clase que asigna un objeto también sea responsable de desasignarlo.

Usar const puede ayudar mucho en algunos casos.Si una función no modifica un objeto y no almacena una referencia que persista después de su regreso, acepte una referencia constante.Al leer el código de la persona que llama, será obvio que su función no ha aceptado la propiedad del objeto.Podría haber hecho que la misma función aceptara un puntero no constante, y la persona que llama puede haber asumido o no que la persona que llama aceptó la propiedad, pero con una referencia constante no hay duda.

No utilice referencias no constantes en listas de argumentos.No está muy claro al leer el código de la persona que llama que la persona que llama puede haber mantenido una referencia al parámetro.

No estoy de acuerdo con los comentarios que recomiendan punteros contados por referencia.Esto normalmente funciona bien, pero cuando tienes un error y no funciona, especialmente si tu destructor hace algo no trivial, como en un programa multiproceso.Definitivamente intente ajustar su diseño para que no necesite contar referencias si no es demasiado difícil.

Consejos en orden de importancia:

-Consejo#1 Recuerde siempre declarar sus destructores "virtuales".

-Consejo#2 Utilice RAII

-Consejo#3 Utilice los punteros inteligentes de boost

-Consejo#4 No escribas tus propios Smartpointers con errores, usa boost (en un proyecto en el que estoy ahora no puedo usar boost, y he sufrido al tener que depurar mis propios punteros inteligentes, definitivamente no los usaría). la misma ruta nuevamente, pero nuevamente en este momento no puedo agregar impulso a nuestras dependencias)

-Consejo#5 Si se trata de un trabajo casual/que no es crítico para el rendimiento (como en juegos con miles de objetos), mire el contenedor de puntero de impulso de Thorsten Ottosen.

-Consejo n.º 6 Encuentre un encabezado de detección de fugas para la plataforma que elija, como el encabezado "vld" de Visual LeakDetection

Si puede, utilice boostshared_ptr y auto_ptr estándar de C++.Estos transmiten semántica de propiedad.

Cuando devuelve un auto_ptr, le está diciendo a la persona que llama que le está dando la propiedad de la memoria.

Cuando devuelves un share_ptr, le estás diciendo a la persona que llama que tienes una referencia a él y que él toma parte de la propiedad, pero no es únicamente su responsabilidad.

Esta semántica también se aplica a los parámetros.Si la persona que llama le pasa un auto_ptr, le está dando la propiedad.

Otros han mencionado formas de evitar pérdidas de memoria en primer lugar (como punteros inteligentes).Pero una herramienta de creación de perfiles y análisis de la memoria suele ser la única forma de rastrear los problemas de memoria una vez que los tienes.

Memcheck de Valgrind es excelente y gratuito.

Solo para MSVC, agregue lo siguiente en la parte superior de cada archivo .cpp:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

Luego, al depurar con VS2003 o superior, se le informará de cualquier fuga cuando su programa salga (realiza un seguimiento de las novedades/eliminaciones).Es básico, pero me ha ayudado en el pasado.

valgrind (sólo disponible para plataformas *nix) es un comprobador de memoria muy bueno

Si vas a gestionar tu memoria manualmente tienes dos casos:

  1. Creé el objeto (quizás indirectamente, llamando a una función que asigna un nuevo objeto), lo uso (o una función a la que llamo lo usa) y luego lo libero.
  2. Alguien me dio la referencia, así que no debería liberarla.

Si necesita infringir alguna de estas reglas, documentelo.

Se trata de propiedad del puntero.

  • Intente evitar la asignación dinámica de objetos.Siempre que las clases tengan constructores y destructores apropiados, use una variable del tipo de clase, no un puntero a ella, y evitará la asignación y desasignación dinámica porque el compilador lo hará por usted.
    En realidad, ese también es el mecanismo utilizado por los "punteros inteligentes" y algunos de los otros escritores lo denominan RAII ;-).
  • Cuando pase objetos a otras funciones, prefiera los parámetros de referencia a los punteros.Esto evita algunos posibles errores.
  • Declare los parámetros constantes, cuando sea posible, especialmente punteros a objetos.De esa manera los objetos no se pueden liberar "accidentalmente" (excepto si descartas la constante ;-))).
  • Minimice la cantidad de lugares en el programa donde realiza la asignación y desasignación de memoria.MI.gramo.Si asigna o libera el mismo tipo varias veces, escriba una función para él (o un método de fábrica ;-)).
    De esta manera, puede crear resultados de depuración (qué direcciones se asignan y desasignan,...) fácilmente, si es necesario.
  • Utilice una función de fábrica para asignar objetos de varias clases relacionadas desde una sola función.
  • Si sus clases tienen una clase base común con un destructor virtual, puede liberarlas todas usando la misma función (o método estático).
  • Comprueba tu programa con herramientas como purificar (lamentablemente muchos $/€/...).

Puede interceptar las funciones de asignación de memoria y ver si hay algunas zonas de memoria que no se liberan al salir del programa (aunque no es adecuado para todo las aplicaciones).

También se puede hacer en tiempo de compilación reemplazando los operadores por nuevo y eliminar y otras funciones de asignación de memoria.

Por ejemplo mira esto sitio Asignación de memoria de depuración en C ++] Nota:Hay un truco para eliminar el operador también algo como este:

#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE

Puede almacenar en algunas variables el nombre del archivo y cuando el operador de eliminación sobrecargado sabrá desde cuál fue el lugar desde el que se llamó.De esta manera puede tener el seguimiento de cada eliminación y malloc de su programa.Al final de la secuencia de verificación de memoria, debería poder informar qué bloque de memoria asignado no se "eliminó", identificándolo por nombre de archivo y número de línea, que supongo que es lo que desea.

También puedes probar algo como Comprobador de límites en Visual Studio, que es bastante interesante y fácil de usar.

Envolvemos todas nuestras funciones de asignación con una capa que agrega una breve cadena al frente y una bandera centinela al final.Entonces, por ejemplo, tendrías una llamada a "myalloc( pszSomeString, iSize, iAlignment );o nuevo( "descripción", iSize ) MiObjeto();que asigna internamente el tamaño especificado más espacio suficiente para su encabezado y centinela.¡Por supuesto, no olvides comentar esto para compilaciones que no sean de depuración!Se necesita un poco más de memoria para hacer esto, pero los beneficios superan con creces los costos.

Esto tiene tres beneficios: en primer lugar, le permite rastrear fácil y rápidamente qué código se está filtrando, realizando búsquedas rápidas de código asignado en ciertas 'zonas' pero que no se han limpiado cuando esas zonas deberían haberse liberado.También puede resultar útil detectar cuándo se ha sobrescrito un límite comprobando que todos los centinelas estén intactos.Esto nos ha salvado numerosas veces al intentar encontrar fallos bien ocultos o errores en la matriz.El tercer beneficio es el seguimiento del uso de la memoria para ver quiénes son los grandes jugadores: una recopilación de ciertas descripciones en un MemDump le indica cuándo el 'sonido' está ocupando mucho más espacio del previsto, por ejemplo.

C++ está diseñado RAII teniendo en cuenta.Creo que realmente no hay mejor manera de administrar la memoria en C++.Pero tenga cuidado de no asignar fragmentos muy grandes (como objetos de búfer) en el ámbito local.Puede causar desbordamientos de pila y, si hay una falla en la verificación de límites mientras se usa ese fragmento, puede sobrescribir otras variables o direcciones de retorno, lo que conduce a todo tipo de agujeros de seguridad.

Uno de los únicos ejemplos sobre cómo asignar y destruir en diferentes lugares es la creación de subprocesos (el parámetro que usted pasa).Pero incluso en este caso es fácil.Aquí está la función/método para crear un hilo:

struct myparams {
int x;
std::vector<double> z;
}

std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...

Aquí, en cambio, la función de hilo.

extern "C" void* th_func(void* p) {
   try {
       std::auto_ptr<myparams> param((myparams*)p);
       ...
   } catch(...) {
   }
   return 0;
}

Bastante fácil, ¿no?En caso de que falle la creación del hilo, el recurso será liberado (eliminado) por auto_ptr; de lo contrario, la propiedad se transferirá al hilo.¿Qué pasa si el hilo es tan rápido que después de la creación libera el recurso antes de que

param.release();

¿Se llama en la función/método principal?¡Nada!Porque le 'diremos' al auto_ptr que ignore la desasignación.¿Es fácil la gestión de la memoria de C++, no?Salud,

¡Ema!

Administre la memoria de la misma manera que administra otros recursos (identificadores, archivos, conexiones de base de datos, sockets...).GC tampoco te ayudaría con ellos.

Exactamente un retorno de cualquier función.De esa manera, puedes desasignar allí y no perderte nada.

De lo contrario, es muy fácil cometer un error:

new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top