web-development-kb-es.site

Pautas generales para evitar pérdidas de memoria en C++

¿Cuáles son algunos consejos generales para asegurarse de que no pierda memoria en los programas de C++? ¿Cómo puedo averiguar quién debe liberar memoria que se ha asignado dinámicamente?

124
dulipishi

En lugar de administrar la memoria manualmente, intente usar punteros inteligentes donde corresponda.
Eche un vistazo a Boost lib , TR1 , y punteros inteligentes .
También los punteros inteligentes ahora forman parte del estándar C++ denominado C++ 11 .

37
Andri Möll

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

No escribas

Object* x = new Object;

o incluso

shared_ptr<Object> x(new Object);

cuando solo puedes escribir

Object x;
194
Ross Smith

Utilice RAII

  • Olvida la recolección de basura (Usa RAII en su lugar). Tenga en cuenta que incluso el recolector de basura también puede filtrarse (si olvida "anular" algunas referencias en Java/C #), y que el recolector de basura no le ayudará a disponer de recursos (si tiene un objeto que adquirió un identificador para un archivo, el archivo no se liberará automáticamente cuando el objeto saldrá del ámbito si no lo hace manualmente en Java, o si utiliza el patrón "disponer" en C #).
  • Olvídese de la regla de "un retorno por función" . Este es un buen consejo de C para evitar fugas, pero está desactualizado en C++ debido a su uso de excepciones (use RAII en su lugar).
  • Y mientras el "Patrón de emparedado" es un buen consejo de C, it está desactualizado en C++ debido a su uso de excepciones (use RAII en su lugar).

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

Aprenda a usar punteros inteligentes, tanto de boost, TR1 o incluso del bajo (pero a menudo lo suficientemente eficiente) auto_ptr (pero debe conocer sus limitaciones).

RAII es la base de la excepción de seguridad y eliminación de recursos en C++, y ningún otro patrón (sándwich, etc.) le proporcionará ambos (y la mayoría de las veces, no le dará ninguno).

Vea a continuación una comparación de 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 de ogro33), RAII se basa en tres conceptos:

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

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

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

Editar (2012-02-12)

"los objetos con alcance ... serán destruidos ... no importa la salida" eso no es del todo cierto. Hay formas de engañar a RAII. cualquier sabor de terminate () pasará por alto la limpieza. exit (EXIT_SUCCESS) es un oxímoron en este sentido.

- wilhelmtell

wilhelmtell tiene toda la razón al respecto: hay excepcional formas de engañar a RAII, lo que lleva al proceso a una parada abrupta.

Esos son excepcionales maneras porque el código C++ no está lleno de terminate, exit, etc., o en el caso de las excepciones, queremos una excepción no controlada para bloquear el proceso y el núcleo descargar su memoria Imagen como está, y no después de la limpieza.

Pero aún debemos saber sobre esos casos porque, si bien rara vez ocurren, todavía pueden suceder.

(¿Quién llama a terminate o exit en código casual de C++? ... Recuerdo que tuve que lidiar con ese problema cuando jugaba con GLUT : las cosas difíciles para los desarrolladores de C++ les gusta no preocuparse por apilar datos asignados o tener decisiones "interesantes" sobre nunca regresar de su bucle principal ... No comentaré sobre eso).

100
paercebal

Querrá ver los punteros inteligentes, como los punteros inteligentes de boost .

En lugar de

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

boost :: shared_ptr eliminará automáticamente una vez que el recuento de referencia 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. Si tiene varios usuarios de su objeto, no tendrá que hacer un seguimiento de si el objeto todavía está en uso. Una vez que nadie se refiere a su puntero compartido, se destruye.

Esto no es una panacea, sin embargo. Aunque puede acceder al puntero de la base, no querría pasarlo a una API de terceros a menos que estuviera seguro de lo que estaba haciendo. Muchas veces, su material de "publicación" en algún otro hilo para el trabajo a realizar DESPUÉS de que finalice el ámbito de 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, usa tu gorra pensante con cualquier herramienta ...

25
Doug T.

Lee sobre RAII y asegúrate de que lo entiendes.

12
Hank

La mayoría de las fugas de memoria son el resultado de no ser claro acerca de la propiedad y la vida útil del objeto.

Lo primero que debe hacer es asignar en la Pila siempre que pueda. Esto se ocupa de la mayoría de los casos en los que necesita asignar un solo objeto para algún propósito.

Si necesita 'nuevo' un objeto, la mayoría de las veces tendrá un único propietario obvio por el resto de su vida útil. 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 el puntero. Se implementan con los contenedores de vectores y mapas STL, pero tienen algunas diferencias:

  • Estas colecciones no pueden ser copiadas o asignadas a. (Una vez que contienen objetos.)
  • Los punteros a los objetos se insertan en ellos.
  • Cuando se elimina la colección, se llama primero al destructor en todos los objetos de la colección. (Tengo otra versión donde se afirma si se destruye y no está vacía).
  • Dado que almacenan punteros, también puede almacenar objetos heredados en estos contenedores.

Mi experiencia con STL es que está tan enfocada 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.

11
Jeroen Dirks

Bah, ustedes, niños pequeños y sus nuevos recolectores de basura ...

Normas muy estrictas sobre la "propiedad": qué objeto o parte del software tiene derecho a eliminar el objeto. Los comentarios claros y los nombres de variables inteligentes hacen que sea obvio si un puntero "posee" o es "simplemente mire, no toque". Para ayudar a decidir quién posee qué, siga lo más posible el patrón de "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; Pienso duro para evitar eso.

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

Muchos otros punteros se definen según sea necesario cada vez que una entidad necesita acceder a otra, para explorar arays o lo que sea; estos son los "solo mirando". Para el ejemplo de escena 3D, un objeto usa una textura pero no es propio; Otros objetos pueden usar esa misma textura. La destrucción de un objeto hace no invoca la destrucción de cualquier textura.

Sí, es mucho tiempo, pero eso es lo que hago. Rara vez tengo fugas 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 a menudo trato transacciones como en banca y comercio electrónico, GUI impulsadas por eventos o alto caos asíncrono en red. ¡Tal vez las nuevas formas tengan una ventaja allí!

10
DarenW

Gran pregunta

si está utilizando c ++ y está desarrollando una aplicación Boud de memoria y memoria en tiempo real (como juegos), necesita escribir su propio Administrador de memoria.

Creo que lo mejor que puedes hacer es fusionar algunos trabajos interesantes de varios autores, puedo darte una pista:

  • El asignador de tamaño fijo se discute ampliamente, en todas partes de la red

  • Alexandrescu introdujo la asignación de objetos pequeños en 2001 en su libro perfecto "Modern c ++ design"

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

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

No comience a escribir un noob inutilizador de asignaciones por su cuenta ... DOCUMENTO DE USTED MISMO primero.

8
ugasoft

Una técnica que se ha hecho popular con la administración de memoria en C++ es RAII . Básicamente se usan constructores/destructores para manejar la asignación de recursos. Por supuesto, hay algunos otros detalles molestos en C++ debido a la excepción de seguridad, pero la idea básica es bastante simple.

El problema generalmente se reduce a uno de propiedad. Recomiendo leer la serie Effective C++ de Scott Meyers y Modern C++ Design de Andrei Alexandrescu.

5
Jason Dagit

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

5
fabiopedrosa

¡Punteros inteligentes para el usuario en todas partes! Clases enteras de fugas de memoria simplemente desaparecen.

4
DougN

Comparte y conoce las reglas de propiedad de la memoria en tu proyecto. El uso de las reglas COM proporciona la mejor consistencia ([in] los parámetros son propiedad de la persona que llama, la persona que llama debe copiar; [out] los parámetros son propiedad de la persona que llama;

4
Seth Morris

valgrind es una buena herramienta para verificar las fugas de memoria de sus programas en tiempo de ejecución, también.

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

Si utiliza para escribir pruebas unitarias para sus programas, debe acostumbrarse a ejecutar sistemáticamente valgrind en las pruebas. Potencialmente evitará muchas fugas de memoria en una etapa temprana. También suele ser más fácil localizarlos en pruebas simples que en un software completo.

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

4
Joseph

Además, no use la memoria asignada manualmente si hay una clase de biblioteca estándar (por ejemplo, vector). Asegúrese de que si viola esa regla que tiene un destructor virtual.

3
Joseph

Si no puedes/no usas un puntero inteligente para algo (aunque debería ser una gran bandera roja), escribe tu código con:

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

Eso es obvio, pero asegúrese de escribirlo antes escriba cualquier código en el alcance

2
Seth Morris

Consejos en orden de importancia:

-Tip # 1 Siempre recuerde declarar sus destructores "virtuales".

-Tip # 2 Use RAII

-Tip # 3 Usa los smartpointers de boost

-Tip # 4 No escriba sus propios Smartpointers con errores, use boost (en un proyecto en el que estoy trabajando en este momento no puedo usar boost, y he tenido que depurar mis propios punteros inteligentes, definitivamente no lo tomaría la misma ruta de nuevo, pero ahora otra vez ahora no puedo agregar impulso a nuestras dependencias)

-Tip # 5 Si es algo casual/no crítico para el rendimiento (como en juegos con miles de objetos), analice el contenedor de punteros de impulso de Thorsten Ottosen

-Tip # 6 Encuentre un encabezado de detección de fugas para su plataforma de elección, como el encabezado "vld" de Visual Leak Detection

2
Robert Gould

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

Deje que el caso donde la función toma posesión del objeto sea el caso especial. En todas las situaciones en que esto suceda, asegúrese de escribir un comentario junto a la función en el archivo de encabezado que indique esto. Debe esforzarse por asegurarse de que en la mayoría de los casos el módulo o la 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 a ella que persiste después de que regrese, 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 tenido la misma función de aceptar un puntero no constante, y la persona que llama puede o no haber asumido que la persona aceptada aceptó la propiedad, pero con una referencia constante no hay duda.

No utilice referencias no constantes en las listas de argumentos. Es muy poco 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 los punteros contados de referencia. Esto generalmente funciona bien, pero cuando tiene un error y no funciona, especialmente si su destructor hace algo no trivial, como en un programa de multiproceso. Definitivamente trate de ajustar su diseño para que no necesite contar con referencias si no es demasiado difícil.

2
Jonathan

Si puede, use boost shared_ptr y standard C++ auto_ptr. Los que 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 devuelve un shared_ptr, le está diciendo a la persona que llama que tiene una referencia a él y que forma parte de la propiedad, pero no es únicamente su responsabilidad.

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

1
Justin Rudd
  • Intenta evitar asignar objetos dinámicamente. Siempre que las clases tengan constructores y destructores apropiados, use una variable del tipo de clase, no un puntero, y evitará la asignación dinámica y la desasignación porque el compilador lo hará por usted.
    En realidad, ese es también el mecanismo utilizado por los "punteros inteligentes" y conocido como RAII por algunos de los otros escritores ;-).
  • Cuando pase objetos a otras funciones, prefiera los parámetros de referencia a los punteros. Esto evita algunos posibles errores.
  • Declare los parámetros const, cuando sea posible, especialmente los punteros a los objetos. De esa manera, los objetos no pueden ser liberados "accidentalmente" (excepto si lanzas la constante ;-))).
  • Minimice la cantidad de lugares en el programa donde realiza la asignación de memoria y la desasignación. P.ej. si asigna o libera el mismo tipo varias veces, escriba una función para ello (o un método de fábrica ;-)).
    De esta manera puede crear resultados de depuración (qué direcciones se asignan y se desasignan, ...) fácilmente, si es necesario.
  • Use 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).
  • Verifique su programa con herramientas como purificar (desafortunadamente muchos $/€/...).
1
mh.

Si va a administrar su memoria manualmente, tiene dos casos:

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

Si necesita romper alguna de estas reglas, documéntelo.

Se trata de la propiedad puntero.

1
Null303

valgrind (solo disponible para plataformas * nix) es un comprobador de memoria muy agradable

1
Ronny Brendel

Otros han mencionado formas de evitar las fugas de memoria en primer lugar (como los punteros inteligentes). Pero una herramienta de análisis de perfiles y memoria es a menudo la única forma de rastrear los problemas de memoria una vez que los tiene.

Valgrind memcheck es una excelente aplicación gratuita.

1
eli

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

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

Luego, cuando realice la depuración con VS2003 o superior, se le informará de cualquier pérdida cuando su programa salga (hace un seguimiento de nuevo/eliminar). Es básico, pero me ha ayudado en el pasado.

1
Rob

C++ está diseñado RAII en mente. Realmente no hay mejor manera de administrar la memoria en C++, creo. 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 al usar esa parte, puede sobrescribir otras variables o direcciones de retorno, lo que lleva a todos los tipos de agujeros de seguridad.

0
artificialidiot

Uno de los únicos ejemplos de asignación y destrucción en diferentes lugares es la creación de subprocesos (el parámetro que pasa). Pero incluso en este caso es fácil. Aquí está la función/método creando 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 la creación del hilo falle, el recurso auto_ptr liberará (eliminará) el recurso, de lo contrario, la propiedad pasará al hilo. ¿Qué pasa si el hilo es tan rápido que después de la creación libera el recurso antes de la

param.release();

se llama en la función principal/método? ¡Nada! Porque "le diremos" al auto_ptr que ignore la desasignación. ¿Es fácil la administración de memoria C++? Aclamaciones,

Ema!

0
Emanuele Oriani

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

0
Nemanja Trifunovic

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 todas las aplicaciones).

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

Por ejemplo, consulte este sitio [Depuración de la asignación de memoria en C++] Nota: También hay un truco para eliminar el operador:

#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 borrado sobrecargado sabrá de qué lugar fue llamado. De esta manera puede tener el seguimiento de cada eliminación y malloc de su programa. Al final de la secuencia de comprobació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 es lo que quiero.

También puedes probar algo como BoundsChecker bajo Visual Studio, que es bastante interesante y fácil de usar.

0
INS

Envolvemos todas nuestras funciones de asignación con una capa que agrega una cadena breve en el frente y una bandera de centinela al final. Así, por ejemplo, tendría una llamada a "myalloc (pszSomeString, iSize, iAlignment); o new (" description ", iSize) MyObject (), que asigna internamente el tamaño especificado más el espacio suficiente para su cabecera y centinela. Por supuesto ¡No olvide comentar esto para compilaciones sin 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: primero, le permite realizar un seguimiento fácil y rápido del código que se está filtrando, al realizar búsquedas rápidas para el código asignado en ciertas 'zonas', pero no se puede limpiar cuando esas zonas deberían haberse liberado. También puede ser útil detectar cuándo se ha sobrescrito un límite al verificar que todos los centinelas estén intactos. Esto nos ha salvado muchas veces cuando intentamos encontrar esos errores bien ocultos o errores de matriz. El tercer beneficio está en rastrear el 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 que anticipó, por ejemplo.

0
screenglow