Directrices generales para evitar fugas de memoria en C++
memory memory-management (29)
Usa RAII
- Olvídese de la recolección de basura (use RAII en su lugar). Tenga en cuenta que incluso el Garbage Collector puede tener fugas (si olvida "anular" algunas referencias en Java / C #), y ese Garbage Collector no lo 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 use el patrón "disponer" en C #).
- Olvide 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 aunque el "Patrón Sandwich" es un buen consejo para C, 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 para saber es RAII .
Aprenda a utilizar punteros inteligentes, tanto de boost, TR1 o incluso de auto_ptr humilde (pero a menudo eficiente) (pero debe conocer sus limitaciones).
RAII es la base de la seguridad de las excepciones y la eliminación de los recursos en C ++, y ningún otro patrón (sándwich, etc.) le dará a ambos (y la mayoría de las veces, no le dará ninguno).
Vea a continuación una comparación de código RAII y código 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 Ogre Psalm33 ), 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! Haz recursos gratuitos en el destructor.
- ¡Todo se trata de alcances! Los objetos delimitados (consulte el ejemplo doRAIIStatic anterior) se construirán en su declaración, y se destruirán en el momento en que la ejecución abandone el alcance, sin importar la salida (retorno, interrupción, excepción, etc.).
Esto significa que en el código correcto de C ++, la mayoría de los objetos no se construirán con new
y se declararán en la pila. Y para aquellos construidos usando new
, todos serán de alguna manera con alcance (por ejemplo, conectados 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 para algunos objetos en Java que hace un uso intensivo de try
/ finally
para ese caso) ...
Editar (2012-02-12)
"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 () omitirá la limpieza. exit (EXIT_SUCCESS) es un oxímoron en este sentido.
- wilhelmtell
wilhelmtell tiene razón al respecto: hay formas excepcionales de engañar a RAII, todo lo cual lleva al proceso a una parada abrupta.
Esas son formas excepcionales porque el código C ++ no está lleno de terminación, salida, etc., o en el caso de excepciones, queremos que una excepción no controlada bloquee el proceso y el núcleo vacíe su imagen de memoria como está, y no después de la limpieza.
Pero aún debemos saber sobre esos casos porque, aunque raramente ocurren, aún pueden suceder.
(¿Quién llama a terminate
o exit
en código casual de C ++? ... Recuerdo haber tenido que lidiar con ese problema cuando jugaba con GLUT : Esta biblioteca está muy orientada a C, yendo tan lejos como diseñando activamente para dificultar las cosas a los desarrolladores de C ++ sin preocuparse por la pila de datos asignados , o tener decisiones "interesantes" sobre nunca regresar de su ciclo principal ... No voy a comentar sobre eso) .
¿Cuáles son algunos consejos generales para asegurarse de que no pierda memoria en los programas C ++? ¿Cómo averiguo quién debería liberar memoria que se ha asignado dinámicamente?
¡Punteros inteligentes para el usuario donde sea que pueda! Toda clase de pérdidas de memoria simplemente desaparecen.
Además, no use memoria asignada manualmente si hay una clase de biblioteca estándar (por ejemplo, vector). Asegúrese de violar la regla de que tiene un destructor virtual.
Administre la memoria de la misma manera que administra otros recursos (identificadores, archivos, conexiones db, sockets ...). GC tampoco te ayudaría con ellos.
Bah, ustedes, los niños pequeños y sus nuevos 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 variable sabios para que sea obvio si un puntero "posee" o es "solo mira, no toques". 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; pienso difícil evitar eso.
En cualquier programa que requiera estructuras de datos complejas, creo un árbol estricto de objetos que contienen otros objetos, usando punteros de "propietario". Este árbol modela la jerarquía básica de los conceptos de dominio de la aplicación. Ejemplo una escena 3D posee objetos, luces, texturas. Al final de la representación cuando el programa se cierra, hay una forma clara de destruir todo.
Muchos otros indicadores se definen como necesarios cuando una entidad necesita acceder a otra, para escanear arays o lo que sea; estos son los "solo buscando". Para el ejemplo de escena en 3D: un objeto usa una textura pero no posee; otros objetos pueden usar esa misma textura. La destrucción de un objeto no invoca la destrucción de ninguna textura.
Sí, lleva mucho tiempo, pero eso es lo que hago. Raramente tengo fugas de memoria u otros problemas. Pero luego trabajo en la arena limitada de alto rendimiento científico, adquisición de datos y software de gráficos. A menudo no trato transacciones como banca y comercio electrónico, GUIs basadas en eventos o caos asincrónico de alta red. ¡Tal vez las nuevas formas tienen una ventaja allí!
C ++ está diseñado RAII en mente. Realmente no hay una mejor manera de administrar la memoria en C ++, creo. Pero tenga cuidado de no asignar trozos muy grandes (como objetos de memoria intermedia) en el alcance local. Puede causar desbordamientos de pila y, si hay un error en la comprobación de límites al usar ese fragmento, puede sobreescribir otras variables o direcciones de retorno, lo que conduce a todos los agujeros de seguridad de tipo.
Comparta y conozca las reglas de propiedad de memoria en su proyecto. El uso de las reglas COM proporciona la mejor consistencia (los parámetros [en] son propiedad de la persona que llama, el que llama debe copiar; los parámetros [de salida] son propiedad de la persona que llama, el destinatario debe hacer una copia si mantiene una referencia, etc.)
Consejos por orden de importancia:
-Tip # 1 Recuerda siempre declarar tus destructores "virtuales".
TIP # 2 Usa RAII
-Tip # 3 Usa los smartpointers de boost
-Tip # 4 No escribas tus propios Smartpointers buggy, usa boost (en un proyecto estoy en este momento no puedo usar boost, y he tenido que depurar mis propios punteros inteligentes, definitivamente no tomaría la misma ruta otra vez, pero de nuevo en este momento no puedo agregar impulso a nuestras dependencias)
-Consejo # 5 Si se trata de un trabajo casual o no crítico (como en los juegos con miles de objetos), mira el contenedor del indicador 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.
Cubrimos todas nuestras funciones de asignación con una capa que agrega una breve cadena en el frente y una bandera de centinela al final. Entonces, por ejemplo, tendrías una llamada a "myalloc (pszSomeString, iSize, iAlignment); o nueva (" description ", iSize) MyObject (); que internamente asigna el tamaño especificado más suficiente espacio para tu encabezado y centinela. , no se olvide de comentar esto para versiones sin depuración. Se necesita un poco más de memoria para hacerlo, pero los beneficios superan con creces los costos.
Esto tiene tres ventajas: primero, le permite rastrear fácilmente y con facilidad el código que se está filtrando, haciendo búsquedas rápidas de códigos asignados en ciertas ''zonas'' pero no limpiadas 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 ahorrado numerosas veces al tratar de encontrar esos bloqueos bien escondidos o errores en la matriz. El tercer beneficio es rastrear el uso de la memoria para ver quiénes son los grandes jugadores: una recopilación de ciertas descripciones en un MemDump te dice cuando el "sonido" está ocupando mucho más espacio de lo que anticipabas, por ejemplo.
Deberá buscar punteros inteligentes, como los indicadores inteligentes de boost .
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 genial. Por lo tanto, 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.
Sin embargo, esto no es una panacea. Aunque puede acceder al puntero base, no le gustaría pasarlo a una API de un tercero a menos que tenga confianza con lo que estaba haciendo. Muchas veces, su material "de publicación" en otro hilo para que el trabajo se realice DESPUÉS de que el alcance de la creación haya finalizado. 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 límite de pensamiento con cualquier herramienta ...
En lugar de administrar la memoria manualmente, intente utilizar punteros inteligentes cuando corresponda.
Eche un vistazo a Boost lib , TR1 y punteros inteligentes .
También los punteros inteligentes ahora son parte del estándar de C ++ llamado C++11 .
Exactamente un retorno de cualquier función. De esa forma puede desasignar y nunca perderse.
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.
Gran pregunta!
Si está utilizando c ++ y está desarrollando una aplicación en tiempo real de CPU y memoria (como juegos), necesita escribir su propio Administrador de memoria.
Creo que lo mejor que puedes hacer es fusionar algunas obras interesantes de varios autores, puedo darte una pista:
El asignador de tamaño fijo es ampliamente discutido, en todas partes en la red
Small Objects Allocation fue presentado por Alexandrescu en 2001 en su libro perfecto "Modern c ++ design"
Un gran avance (con el código fuente distribuido) se puede encontrar en un artículo sorprendente en Game Programming Gem 7 (2008) llamado "High Performance Heap allocator", escrito por Dimitar Lazarov.
Una gran lista de recursos se puede encontrar en this artículo
No empieces a escribir un asignador inútil de novatos por ti mismo ... DOCUMENT YOURSELF primero.
La mayoría de las pérdidas de memoria son el resultado de no ser claros acerca de la propiedad y la duración del objeto.
Lo primero que debe hacer es asignar en la pila siempre que pueda. Esto se refiere a la mayoría de los casos en los que necesita asignar un único objeto para algún propósito.
Si necesita ''nuevo'' un objeto, la mayor parte del tiempo tendrá un 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 un puntero. Se implementan con el vector STL y los contenedores de mapas, pero tienen algunas diferencias:
- Estas colecciones no se pueden copiar ni asignar. (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 afirma si está destruida y no está vacía).
- Como almacenan punteros, también puede almacenar objetos heredados en estos contenedores.
Mi objetivo con STL es que está tan enfocado en los objetos Value mientras que en la mayoría de las aplicaciones los objetos son entidades únicas que no tienen una semántica de copia significativa requerida para usar en esos contenedores.
Lea sobre RAII y asegúrese de entenderlo.
Otros han mencionado formas de evitar pérdidas de memoria en primer lugar (como punteros inteligentes). Pero una herramienta de análisis de perfiles y memoria a menudo es la única forma de localizar problemas de memoria una vez que los tiene.
Valgrind memcheck es uno excelente y gratuito.
Puede interceptar las funciones de asignación de memoria y ver si hay algunas zonas de memoria no liberadas al salir del programa (aunque no es adecuado para todas las aplicaciones).
También se puede hacer en tiempo de compilación reemplazando los operadores nuevo y eliminar y otras funciones de asignación de memoria.
Por ejemplo, compruebe en este site [Asignación de memoria de depuración en C ++] Nota: Hay un truco para el operador de eliminación también algo como esto:
#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á cuál fue el lugar desde el que se invocó. De esta manera puede tener el rastro de cada eliminación y malloc de su programa. Al final de la secuencia de comprobación de la memoria, debe poder informar qué bloque de memoria asignada no se ''eliminó'', identificándolo por nombre de archivo y número de línea, supongo que lo que quiere.
También podría probar algo como BoundsChecker en Visual Studio, que es bastante interesante y fácil de usar.
Respaldo completamente todos los consejos sobre RAII y los indicadores inteligentes, pero también me gustaría agregar un consejo de un nivel ligeramente superior: la memoria más fácil de administrar es la memoria que nunca asignó. A diferencia de idiomas como C # y Java, donde casi todo es una referencia, en C ++ debes poner objetos en la pila siempre que puedas. Como he visto, varias personas (incluido el Dr. Stroustrup) señalan que la razón principal por la que la recolección de basura nunca ha sido popular en C ++ es que C ++ bien redactado no produce mucha basura en primer lugar.
No escribir
Object* x = new Object;
o incluso
shared_ptr<Object> x(new Object);
cuando puedes escribir
Object x;
Si no puede / no usa un puntero inteligente para algo (aunque debería ser una gran bandera roja), escriba su código con:
allocate
if allocation succeeded:
{ //scope)
deallocate()
}
Eso es obvio, pero asegúrese de escribirlo antes de escribir cualquier código en el alcance
Si puede, use boost shared_ptr y C ++ auto_ptr estándar. Aquellos transmiten semántica de propiedad.
Cuando devuelve un auto_ptr, le está diciendo a la persona que llama que les está otorgando la propiedad de la memoria.
Cuando devuelve un shared_ptr, le está diciendo a la persona que llama que tiene una referencia y que forma 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 otorgan la propiedad.
Si va a administrar su memoria manualmente, tiene dos casos:
- Creé el objeto (quizás indirectamente, llamando a una función que asigna un nuevo objeto), lo uso (o una función que llamo lo usa), luego lo libero.
- Alguien me dio la referencia, así que no debería liberarla.
Si necesita romper alguna de estas reglas, por favor documente.
Se trata de la propiedad del puntero.
Solo para MSVC, agregue lo siguiente a 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 se cierre (rastrea nuevo / eliminar). Es básico, pero me ha ayudado en el pasado.
Una fuente frecuente de estos errores es cuando tienes un método que acepta una referencia o un puntero a un objeto pero deja la propiedad poco clara. Las convenciones de estilo y 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 donde esto sucede, asegúrese de escribir un comentario al lado de la función en el archivo de encabezado que lo indique. Debe esforzarse para 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 que persiste después de que retorna, 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ías haber tenido la misma función de aceptar un puntero no const, y la persona que llama puede haber asumido o no que el destinatario ha aceptado la propiedad, pero con una referencia constante no hay duda.
No use referencias no const en listas de argumentos. No está muy claro al leer el código de llamada que el destinatario puede haber mantenido una referencia al parámetro.
No estoy de acuerdo con los comentarios que recomiendan punteros de referencia contados. Esto generalmente funciona bien, pero cuando tienes un error y no funciona, especialmente si tu destructor hace algo no trivial, como en un programa multiproceso. Definitivamente trate de ajustar su diseño para que no necesite contar de referencia si no es demasiado difícil.
Una técnica que se ha vuelto 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 otros detalles desagradables en C ++ debido a la seguridad de las excepciones, 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.
Uno de los únicos ejemplos sobre asignación y destrucción en diferentes lugares es la creación de subprocesos (el parámetro que se pasa). Pero incluso en este caso es fácil. Aquí está la función / método que crea 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 hilo
extern "C" void* th_func(void* p) {
try {
std::auto_ptr<myparams> param((myparams*)p);
...
} catch(...) {
}
return 0;
}
Bastante easyn no es así? En caso de que la creación del hilo falle, el recurso será liberado (eliminado) por el auto_ptr, de lo contrario la propiedad pasará al hilo. ¿Y 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 / método principal? ¡Nada! Porque ''diremos'' a auto_ptr que ignore la desasignación. ¿Es fácil la administración de la memoria C ++? Aclamaciones,
Ema!
Ya hay mucho sobre cómo no fugas, pero si necesita una herramienta para ayudarlo a rastrear fugas, eche un vistazo a:
- BoundsChecker bajo VS
- MMGR C / C ++ lib de FluidStudio http://www.paulnettle.com/pub/FluidStudios/MemoryManagers/Fluid_Studios_Memory_Manager.zip (anula los métodos de asignación y crea un informe de las asignaciones, fugas, etc.)
valgrind (solo disponible para plataformas * nix) es un corrector de memoria muy agradable
valgrind es una buena herramienta para comprobar las fugas de memoria de sus programas en tiempo de ejecución, también.
Está disponible en la mayoría de los sabores de Linux (incluido Android) y en Darwin.
Si utiliza para escribir pruebas de unidad para sus programas, debe tener el hábito de ejecutar valgrind sistemáticamente en las pruebas. Potencialmente evitará muchas pérdidas de memoria en una etapa temprana. También es generalmente más fácil identificarlos en pruebas simples que en un software completo.
Por supuesto, este consejo sigue siendo válido para cualquier otra herramienta de verificación de memoria.
- 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 a ella, 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 "punteros inteligentes" y algunos de los otros escritores lo llaman RAII ;-). - Cuando pase objetos a otras funciones, prefiera los parámetros de referencia sobre los punteros. Esto evita algunos posibles errores.
- Declare const parámetros, cuando sea posible, especialmente punteros a los objetos. De esta forma los objetos no pueden ser liberados "accidentalmente" (excepto si echas el const lejos ;-))).
- 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 él (o un método de fábrica ;-)).
De esta forma, puede crear resultados de depuración (qué direcciones se asignan y 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 usando la misma función (o método estático).
- Verifica tu programa con herramientas como purificar (lamentablemente muchos $ / € / ...).