¿Por qué el uso de 'nuevo' causa pérdidas de memoria?

Respuestas:

464

Qué está pasando

Cuando escribe T t;, está creando un objeto de tipo Tcon duración de almacenamiento automático . Se limpiará automáticamente cuando salga del alcance.

Cuando escribe new T(), está creando un objeto de tipo Tcon una duración de almacenamiento dinámico . No se limpiará automáticamente.

nuevo sin limpieza

Debes pasarle un puntero deletepara limpiarlo:

nuevo con eliminar

Sin embargo, su segundo ejemplo es peor: está desreferenciando el puntero y haciendo una copia del objeto. De esta manera, pierde el puntero al objeto creado new, por lo que nunca puede eliminarlo, ¡incluso si lo desea!

nuevo con deref

Que deberias hacer

Debe preferir la duración del almacenamiento automático. Necesita un nuevo objeto, solo escriba:

A a; // a new object of type A
B b; // a new object of type B

Si necesita una duración de almacenamiento dinámica, almacene el puntero al objeto asignado en un objeto de duración de almacenamiento automático que lo elimine automáticamente.

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

newing con automatic_pointer

Este es un idioma común que se conoce con el nombre poco descriptivo RAII ( La adquisición de recursos es la inicialización ). Cuando adquiere un recurso que necesita limpieza, lo coloca en un objeto de duración de almacenamiento automático para que no tenga que preocuparse por limpiarlo. Esto se aplica a cualquier recurso, ya sea memoria, archivos abiertos, conexiones de red o lo que desee.

Esto automatic_pointerya existe en varias formas, lo acabo de proporcionar para dar un ejemplo. Existe una clase muy similar en la biblioteca estándar llamada std::unique_ptr.

También hay uno antiguo (anterior a C ++ 11) llamado auto_ptrpero ahora está en desuso porque tiene un comportamiento de copia extraño.

Y luego hay algunos ejemplos aún más inteligentes, como std::shared_ptr, que permiten múltiples punteros al mismo objeto y solo lo limpia cuando se destruye el último puntero.

R. Martinho Fernandes
fuente
44
@ user1131997: me alegra que hayas hecho esta otra pregunta. Como puede ver, no es muy fácil de explicar en los comentarios :)
R. Martinho Fernandes
@ R.MartinhoFernandes: excelente respuesta. Solo una pregunta. ¿Por qué usó return por referencia en la función de operador * ()?
Destructor
@Destructor respuesta tardía: D. Devolver por referencia le permite modificar el puntero, para que pueda hacer, por ejemplo *p += 2, como lo haría con un puntero normal. Si no regresara por referencia, no imitaría el comportamiento de un puntero normal, que es la intención aquí.
R. Martinho Fernandes
Muchas gracias por recomendarnos "almacenar el puntero al objeto asignado en un objeto de duración de almacenamiento automático que lo elimina automáticamente". ¡Ojalá hubiera una forma de exigir a los codificadores que aprendan este patrón antes de que puedan compilar cualquier C ++!
Andy
35

Una explicación paso a paso:

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

Entonces, al final de esto, tiene un objeto en el montón sin puntero, por lo que es imposible eliminarlo.

La otra muestra:

A *object1 = new A();

es una pérdida de memoria solo si olvida deletela memoria asignada:

delete object1;

En C ++ hay objetos con almacenamiento automático, aquellos creados en la pila, que se eliminan automáticamente, y objetos con almacenamiento dinámico, en el montón, con los que asigna newy con los que debe liberarse delete. (todo esto es más o menos)

Piensa que deberías tener un deletepara cada objeto asignado new.

EDITAR

Ahora que lo pienso, object2no tiene que ser una pérdida de memoria.

El siguiente código es solo para hacer un punto, es una mala idea, nunca me gusta un código como este:

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

En este caso, dado que otherse pasa por referencia, será el objeto exacto señalado por new B(). Por lo tanto, obtener su dirección &othery eliminar el puntero liberaría la memoria.

Pero no puedo enfatizar esto lo suficiente, no hagas esto. Solo está aquí para hacer un punto.

Luchian Grigore
fuente
2
Estaba pensando lo mismo: podemos hackearlo para que no se filtre, pero no querrás hacerlo. object1 tampoco tiene que filtrarse, ya que su constructor podría unirse a algún tipo de estructura de datos que lo eliminará en algún momento.
CashCow
2
¡Siempre es TAN tentador escribir las respuestas "es posible hacer esto pero no"! :-) Conozco el sentimiento
Kos
11

Dados dos "objetos":

obj a;
obj b;

No ocuparán la misma ubicación en la memoria. En otras palabras,&a != &b

Asignar el valor de uno a otro no cambiará su ubicación, pero cambiará su contenido:

obj a;
obj b = a;
//a == b, but &a != &b

Intuitivamente, los "objetos" de puntero funcionan de la misma manera:

obj *a;
obj *b = a;
//a == b, but &a != &b

Ahora, veamos tu ejemplo:

A *object1 = new A();

Esto es asignar el valor de new A()a object1. El valor es un puntero, es decir object1 == new A(), pero &object1 != &(new A()). (Tenga en cuenta que este ejemplo no es un código válido, es solo para explicación)

Debido a que se preserva el valor del puntero, podemos liberar la memoria a la que apunta: delete object1;Debido a nuestra regla, esto se comporta de la misma manera delete (new A());que no tiene fugas.


Para su segundo ejemplo, está copiando el objeto señalado. El valor es el contenido de ese objeto, no el puntero real. Como en cualquier otro caso &object2 != &*(new A()),.

B object2 = *(new B());

Hemos perdido el puntero en la memoria asignada y, por lo tanto, no podemos liberarlo. delete &object2;Puede parecer que funcionaría, pero porque &object2 != &*(new A())no es equivalente delete (new A())y, por lo tanto, no es válido.

Pubby
fuente
9

En C # y Java, usa new para crear una instancia de cualquier clase y luego no necesita preocuparse por destruirla más tarde.

C ++ también tiene una palabra clave "nuevo" que crea un objeto, pero a diferencia de Java o C #, no es la única forma de crear un objeto.

C ++ tiene dos mecanismos para crear un objeto:

  • automático
  • dinámica

Con la creación automática, crea el objeto en un entorno de ámbito: - en una función o - como miembro de una clase (o estructura).

En una función, la crearía de esta manera:

int func()
{
   A a;
   B b( 1, 2 );
}

Dentro de una clase, normalmente lo crearía de esta manera:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

En el primer caso, los objetos se destruyen automáticamente cuando se sale del bloque de alcance. Esto podría ser una función o un bloque de alcance dentro de una función.

En el último caso, el objeto b se destruye junto con la instancia de A en la que es miembro.

Los objetos se asignan con nuevo cuando necesita controlar la vida útil del objeto y luego se requiere eliminar para destruirlo. Con la técnica conocida como RAII, usted se encarga de eliminar el objeto en el punto en que lo crea colocándolo dentro de un objeto automático y espera a que el destructor de ese objeto automático surta efecto.

Uno de estos objetos es shared_ptr, que invocará una lógica de "borrador", pero solo cuando se destruyen todas las instancias de shared_ptr que comparten el objeto.

En general, si bien su código puede tener muchas llamadas a nuevo, debe tener llamadas limitadas para eliminar y siempre debe asegurarse de que se llamen desde destructores u objetos "eliminadores" que se colocan en punteros inteligentes.

Sus destructores tampoco deberían arrojar excepciones.

Si hace esto, tendrá pocas pérdidas de memoria.

CashCow
fuente
44
Hay más que automaticy dynamic. También hay static.
Mooing Duck el
9
B object2 = *(new B());

Esta línea es la causa de la fuga. Vamos a separar esto un poco ...

object2 es una variable de tipo B, almacenada en dicha dirección 1 (Sí, estoy eligiendo números arbitrarios aquí). En el lado derecho, ha pedido una nueva B, o un puntero a un objeto de tipo B. El programa con gusto le da esto y le asigna su nueva B a la dirección 2 y también crea un puntero en la dirección 3. Ahora, la única forma de acceder a los datos en la dirección 2 es a través del puntero en la dirección 3. A continuación, desreferenciaron el puntero *para obtener los datos a los que apunta el puntero (los datos en la dirección 2). Esto crea efectivamente una copia de esos datos y los asigna al objeto2, asignado en la dirección 1. Recuerde, es una COPIA, no el original.

Ahora, aquí está el problema:

¡Nunca almacenaste ese puntero en ningún lugar donde puedas usarlo! Una vez que finaliza esta asignación, el puntero (memoria en la dirección 3, que utilizó para acceder a la dirección 2) está fuera del alcance y fuera de su alcance. Ya no puede llamar a eliminar en él y, por lo tanto, no puede limpiar la memoria en la dirección2. Lo que queda es una copia de los datos de la dirección2 en la dirección1. Dos de las mismas cosas sentadas en la memoria. A uno puede acceder, al otro no (porque perdió el camino). Es por eso que esta es una pérdida de memoria.

Sugeriría que, a partir de su fondo de C #, lea mucho sobre cómo funcionan los punteros en C ++. Son un tema avanzado y pueden tomar un tiempo para comprenderlo, pero su uso será invaluable para usted.

MGZero
fuente
8

Si lo hace más fácil, piense en la memoria de la computadora como si fuera un hotel y los programas son clientes que alquilan habitaciones cuando las necesitan.

La forma en que funciona este hotel es que usted reserva una habitación y le dice al portero cuando se va.

Si programa los libros en una habitación y se va sin avisarle al portero, el portero pensará que la habitación todavía está en uso y no permitirá que nadie más la use. En este caso hay una fuga de habitación.

Si su programa asigna memoria y no la elimina (simplemente deja de usarla), la computadora cree que la memoria todavía está en uso y no permitirá que nadie más la use. Esta es una pérdida de memoria.

Esta no es una analogía exacta, pero podría ayudar.

Stefan
fuente
55
Me gusta mucho esa analogía, no es perfecta, ¡pero definitivamente es una buena manera de explicar las pérdidas de memoria a las personas que son nuevas en ella!
AdamM
1
Utilicé esto en una entrevista para un ingeniero sénior en Bloomberg en Londres para explicar las pérdidas de memoria a una chica de recursos humanos. Terminé esa entrevista porque pude explicar las pérdidas de memoria (y los problemas de subprocesos) a una persona que no era programadora de una manera que ella entendió.
Stefan
7

Al crear object2, está creando una copia del objeto que creó con nuevo, pero también está perdiendo el puntero (nunca asignado) (por lo que no hay forma de eliminarlo más adelante). Para evitar esto, tendrías que hacer object2una referencia.

Mario
fuente
3
Es una práctica increíblemente mala tomar la dirección de una referencia para eliminar un objeto. Usa un puntero inteligente.
Tom Whittock el
3
Increíblemente mala práctica, ¿eh? ¿Qué crees que usan los punteros inteligentes detrás de escena?
Blindy
3
Los punteros inteligentes de @Blindy (al menos los implementados decentemente) usan punteros directamente.
Luchian Grigore
2
Bueno, para ser sincero, toda la idea no es tan genial, ¿no? En realidad, ni siquiera estoy seguro de dónde sería útil el patrón probado en el OP.
Mario
7

Bueno, crea una pérdida de memoria si en algún momento no libera la memoria que ha asignado utilizando el newoperador al pasar un puntero a esa memoria al deleteoperador.

En tus dos casos anteriores:

A *object1 = new A();

Aquí no está utilizando deletepara liberar la memoria, por lo que si su object1puntero se sale del alcance, tendrá una pérdida de memoria, porque habrá perdido el puntero y, por lo tanto, no puede usar el deleteoperador.

Y aquí

B object2 = *(new B());

está descartando el puntero devuelto por new B(), por lo que nunca puede pasar ese puntero deletepara que se libere la memoria. De ahí otra pérdida de memoria.

razlebe
fuente
7

Es esta línea la que se está filtrando de inmediato:

B object2 = *(new B());

Aquí está creando un nuevo Bobjeto en el montón, luego creando una copia en la pila. Ya no se puede acceder al que se ha asignado en el montón y, por lo tanto, a la fuga.

Esta línea no tiene fugas inmediatas:

A *object1 = new A();

Habría una fuga si nunca deleteD object1embargo.

mattjgalloway
fuente
44
No utilice el montón / pila cuando explique el almacenamiento dinámico / automático.
Pubby
2
@Pubby, ¿por qué no usar? Debido a que el almacenamiento dinámico / automático siempre es dinámico, ¿no apilable? Y es por eso que no hay necesidad de detallar sobre stack / heap, ¿estoy en lo cierto?
44
@ user1131997 El montón / pila son detalles de implementación. Es importante conocerlos, pero son irrelevantes para esta pregunta.
Pubby
2
Hmm, me gustaría una respuesta por separado, es decir, igual que la mía, pero reemplazando el montón / pila con lo que mejor te parezca. Me interesaría saber cómo preferirías explicarlo.
mattjgalloway