¿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?
¿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?
Respuestas:
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 C ++ llamado C ++ 11 .
Respaldo a fondo todos los consejos sobre RAII y los punteros inteligentes, pero también me gustaría agregar un consejo de nivel ligeramente más alto: 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 que varias personas (incluido el Dr. Stroustrup) señalan, 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;
Esta publicación parece ser repetitiva, pero en C ++, el patrón más básico para saber es RAII .
Aprenda a usar punteros inteligentes, tanto desde boost, TR1 o incluso el auto_ptr bajo (pero a menudo lo suficientemente eficiente) (pero debe conocer sus limitaciones).
RAII es la base de la seguridad de excepción y la 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.
}
Para resumir (después del comentario de Ogre Psalm33 ), RAII se basa en tres conceptos:
Esto significa que en el código C ++ correcto, la mayoría de los objetos no se construirán new
y se declararán en la pila. Y para aquellos construidos usando new
, todos tendrán un 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 para algunos objetos en Java que hacen un uso intensivo de try
/ finally
para ese caso) ...
"los objetos con alcance ... serán destruidos ... sin importar la salida", eso no es del todo cierto. Hay formas de engañar a RAII. cualquier sabor de terminar () omitirá la limpieza. exit (EXIT_SUCCESS) es un oxímoron en este sentido.
Wilhelmtell tiene toda la razón al respecto: hay formas excepcionales de engañar a la RAII, todo lo cual lleva a que el proceso se detenga abruptamente.
Esas son formas excepcionales porque el código C ++ no está lleno de terminación, salida, etc., o en el caso de excepciones, queremos una excepción no controlada para bloquear el proceso y volcar su imagen de memoria como está, y no después de la limpieza.
Pero aún debemos conocer esos casos porque, aunque rara vez suceden, aún pueden suceder.
(¿quién llama terminate
o exit
en código C ++ casual? ... Recuerdo haber tenido que lidiar con ese problema cuando jugaba con GLUT : esta biblioteca está muy orientada a C, yendo tan lejos como a diseñarla activamente para dificultar las cosas a los desarrolladores de C ++ como no preocuparse sobre la asignación de datos asignados , o tener decisiones "interesantes" sobre no volver nunca de su ciclo principal ... no comentaré sobre eso) .
Querrá ver los punteros inteligentes, como los punteros inteligentes de boost .
En vez de
int main()
{
Object* obj = new Object();
//...
delete obj;
}
boost :: shared_ptr se 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 genial. Entonces, 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 refiera 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 estaba haciendo. Muchas veces, su "publicación" de cosas en algún otro hilo para el trabajo que se realizará DESPUÉS de que finalice la creación del alcance. 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 de pensamiento con cualquier herramienta ...
La mayoría de las pérdidas de memoria son el resultado de no ser claros acerca de la propiedad y la vida útil de los objetos.
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 para 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 ellos por puntero. Se implementan con el vector STL y los contenedores de mapas, pero tienen algunas diferencias:
Mi ventaja 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.
Bah, ustedes, 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 sabios de variables para que sea obvio si un puntero "posee" o es "solo mira, no toques". Para ayudar a decidir quién posee qué, siga tanto como sea posible el patrón de "emparedado" 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 de dominio de 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 punteros se definen como necesarios cuando una entidad necesita acceder a otra, para escanear sobre arays o lo que sea; estos son los "solo mirando". Para el ejemplo de escena 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í, consume mucho tiempo, pero eso es lo que hago. Raramente 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. A menudo no trato transacciones como en la banca y el comercio electrónico, las GUI controladas por eventos o el caos asincrónico de alta red. ¡Quizás las formas novedosas tienen una ventaja allí!
Gran pregunta!
si está utilizando c ++ y está desarrollando una aplicación de CPU y memoria en tiempo real (como juegos), debe 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 es ampliamente discutido, en todas partes en la red
La asignación de objetos pequeños fue presentada por Alexandrescu en 2001 en su libro perfecto "Diseño moderno de c ++"
Se puede encontrar un gran avance (con el código fuente distribuido) en un sorprendente artículo en Game Programming Gem 7 (2008) llamado "High Performance Heap allocator", escrito por Dimitar Lazarov
Puede encontrar una gran lista de recursos en este artículo
No empiece a escribir un novato asignador inútil por sí mismo ... DOCUMENTARSE primero.
Una técnica que se ha vuelto popular con la administración de memoria en C ++ es RAII . Básicamente utiliza constructores / destructores para manejar la asignación de recursos. Por supuesto, hay algunos otros detalles desagradables en C ++ debido a la seguridad de excepción, 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.
Ya hay mucho sobre cómo no tener fugas, pero si necesita una herramienta que lo ayude a rastrear fugas, eche un vistazo a:
Comparta y conozca las reglas de propiedad de la memoria en su proyecto. El uso de las reglas COM proporciona la mejor consistencia (los parámetros [in] son propiedad de la persona que llama, la persona que llama debe copiar; los parámetros [out] son propiedad de la persona que llama, la persona que llama debe hacer una copia si mantiene una referencia; etc.)
valgrind es una buena herramienta para verificar las pérdidas de memoria de sus programas también en tiempo de ejecución.
Está disponible en la mayoría de los sabores de Linux (incluido Android) y en Darwin.
Si sueles escribir pruebas unitarias para tus programas, debes acostumbrarte a ejecutar sistemáticamente valgrind 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 simples que en un software completo.
Por supuesto, este consejo es válido para cualquier otra herramienta de verificación de memoria.
Si no puede / no usa un puntero inteligente para algo (aunque eso 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
Una fuente frecuente de estos errores es cuando tiene un método que acepta una referencia o puntero a un objeto pero deja en claro la propiedad. Las convenciones de estilo y comentarios pueden hacer que esto sea 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 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 modificará un objeto y no almacena una referencia 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 sin 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 use referencias no constantes en las listas de argumentos. Al leer el código de la persona que llama, no está claro que la persona que llamó pudo haber mantenido una referencia al parámetro.
No estoy de acuerdo con los comentarios que recomiendan 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 multiproceso. Definitivamente intente ajustar su diseño para que no necesite un recuento de referencias si no es demasiado difícil.
Consejos en orden de importancia:
-Tip # 1 Recuerde siempre declarar sus destructores "virtuales".
-Tip # 2 Use RAII
-Tip # 3 Usa los punteros inteligentes de boost
- Consejo # 4 No escriba sus propios Smartpointers con errores, use boost (en un proyecto en el que estoy ahora no puedo usar boost, y he sufrido la necesidad de depurar mis propios punteros inteligentes, definitivamente no tomaría la misma ruta nuevamente, pero de nuevo en este momento no puedo agregar impulso a nuestras dependencias)
-Consejo # 5 Si es algo casual / no crítico para el rendimiento (como en juegos con miles de objetos) funciona, mira el contenedor de puntero de impulso de Thorsten Ottosen
Consejo # 6 Encuentre un encabezado de detección de fugas para su plataforma de elección, como el encabezado "vld" de Visual Leak Detection
Si puede, use boost shared_ptr y C ++ auto_ptr estándar. Los que transmiten la 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 y que forma parte de la propiedad, pero no es responsabilidad exclusiva de ellos.
Esta semántica también se aplica a los parámetros. Si la persona que llama le pasa un auto_ptr, le están dando la propiedad.
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 es a menudo la única forma de localizar problemas de memoria una vez que los tiene.
Valgrind memcheck es excelente y gratuito.
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 salga (rastrea nuevo / eliminar). Es básico, pero me ha ayudado en el pasado.
valgrind (solo disponible para plataformas * nix) es un muy buen corrector de memoria
Si va a administrar su memoria manualmente, tiene dos casos:
Si necesita romper alguna de estas reglas, por favor documente.
Se trata de la propiedad del puntero.
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 new y delete y otras funciones de asignación de memoria.
Por ejemplo, consulte en este sitio [Depuración de la asignación de memoria en C ++] Nota: Hay un truco para eliminar el operador 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 sepa de dónde fue llamado. 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 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 puede probar algo como BoundsChecker 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ía una llamada a "myalloc (pszSomeString, iSize, iAlignment); o new (" description ", iSize) MyObject (); que asigna internamente el tamaño especificado más espacio suficiente para su encabezado 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 rastrear fácil y rápidamente qué código tiene fugas, haciendo búsquedas rápidas para el código asignado en ciertas 'zonas' pero no limpiado cuando esas zonas deberían haberse liberado. También puede ser útil detectar cuándo se ha sobrescrito un límite verificando que todos los centinelas estén intactos. Esto nos ha salvado numerosas veces al tratar de encontrar esos bloqueos bien ocultos o pasos en falso de 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 cuándo, por ejemplo, el "sonido" ocupa mucho más espacio de lo que esperabas.
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 comprobación de límites al usar ese fragmento, puede sobrescribir otras variables o direcciones de retorno, lo que conduce a agujeros de seguridad de todo tipo.
Uno de los únicos ejemplos sobre la 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 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 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 será liberado (eliminado) por el auto_ptr; 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
param.release();
se llama en la función / método principal? ¡Nada! Porque 'le diremos' al auto_ptr que ignore la desasignación. ¿La administración de memoria C ++ es fácil, no? Salud,
Ema!
Administre la memoria de la misma manera que administra otros recursos (identificadores, archivos, conexiones db, sockets ...). GC tampoco te ayudaría con ellos.
Exactamente un retorno de cualquier función. De esa manera, puede hacer la desasignación allí 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.