c++ - programacion - ¿Por qué el uso de "nuevo" causa pérdidas de memoria?
manual de programacion android pdf (9)
Aprendí C # primero, y ahora estoy empezando con C ++. Según entiendo, el operador new
en C ++ no es similar al que está en C #.
¿Puedes explicar el motivo de la pérdida de memoria en este código de muestra?
class A { ... };
struct B { ... };
A *object1 = new A();
B object2 = *(new B());
Al crear object2
está creando una copia del objeto que creó con new, 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 object2
una referencia.
Bueno, crea una pérdida de memoria si en algún momento no libera la memoria que ha asignado usando el new
operador pasando un puntero a esa memoria al operador de delete
.
En sus dos casos anteriores:
A *object1 = new A();
Aquí no está utilizando delete
para liberar la memoria, por lo que si su puntero object1
queda fuera del alcance, tendrá una pérdida de memoria, porque habrá perdido el puntero y no podrá usar el operador delete
en eso.
Y aquí
B object2 = *(new B());
está descartando el puntero devuelto por la new B()
, por lo que nunca puede pasar ese puntero para delete
la memoria que se liberará. De ahí otra fuga de memoria.
Dado dos "objetos":
obj a;
obj b;
No ocuparán el mismo lugar en la memoria. En otras palabras, &a != &b
Asignar el valor de uno al 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 está asignando el valor de new A()
a object1
. El valor es un puntero, que significa object1 == new A()
, pero &object1 != &(new A())
. (Tenga en cuenta que este ejemplo no es un código válido, es sólo para la explicación)
Como el valor del puntero se conserva, podemos liberar la memoria a la que apunta: delete object1;
Debido a nuestra regla, esto se comporta de la misma manera que delete (new A());
que no tiene fuga
Para el segundo ejemplo, está copiando el objeto apuntado. 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 a la memoria asignada, y por lo tanto no podemos liberarlo. delete &object2;
puede parecer que funcionaría, pero como &object2 != &*(new A())
, no es equivalente a delete (new A())
y por lo tanto no es válido.
En C # y Java, usa lo nuevo para crear una instancia de cualquier clase y luego no tiene que preocuparse por destruirlo más tarde.
C ++ también tiene una palabra clave "nueva" 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, usted 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ías de esta manera:
int func()
{
A a;
B b( 1, 2 );
}
Dentro de una clase normalmente lo crearías 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 este ú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 del objeto y luego requiere eliminar para destruirlo. Con la técnica conocida como RAII, se ocupa 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 del objeto automático surta efecto.
Uno de estos objetos es un shared_ptr que invocará una lógica "deleter" pero solo cuando se destruyan todas las instancias de shared_ptr que están compartiendo el objeto.
En general, aunque su código tenga muchas llamadas a nuevas, debe tener llamadas limitadas para eliminar y siempre debe asegurarse de llamarlas desde destructores o objetos "delegadores" que se colocan en punteros inteligentes.
Tus destructores tampoco deberían lanzar excepciones.
Si haces esto, tendrás pocas pérdidas de memoria.
Es esta línea la que está goteando inmediatamente:
B object2 = *(new B());
Aquí está creando un nuevo objeto B
en el montón, y luego crea una copia en la pila. El que se ha asignado en el montón ya no se puede acceder y, por lo tanto, la fuga.
Esta línea no tiene fugas de inmediato:
A *object1 = new A();
Sin embargo, si nunca object1
d object1
, habrá una pérdida.
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 reserve una habitación y le cuente al portero cuando se vaya.
Si programa libros en una habitación y se va sin avisar al portero, el portero pensará que la habitación está en uso y no permitirá que nadie más la use. En este caso, hay una fuga de sala.
Si su programa asigna memoria y no la elimina (simplemente deja de usarla), la computadora piensa 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.
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 de eliminar.
La otra muestra:
A *object1 = new A();
es una pérdida de memoria solo si olvida delete
la 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, que se asignan con new
y son necesarios para liberarse con la delete
. (Esto es todo más o menos)
Piensa que deberías tener una delete
para cada objeto asignado con new
.
EDITAR
Ahora que lo pienso, object2
no tiene que ser una pérdida de memoria.
El siguiente código es solo para hacer un punto, es una mala idea, nunca te guste 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 other
se pasa por referencia, será el objeto exacto señalado por el new B()
. Por lo tanto, obtener su dirección por &other
y borrar el puntero liberaría la memoria.
Pero no puedo enfatizar esto lo suficiente, no hagas esto. Está aquí para hacer un punto.
Que esta pasando
Cuando escribes T t;
está creando un objeto de tipo T
con duración de almacenamiento automática . Se limpiará automáticamente cuando se salga del alcance.
Cuando escribe una new T()
está creando un objeto de tipo T
con una duración de almacenamiento dinámica . No se limpiará automáticamente.
Debes pasarle un puntero para delete
y limpiarlo:
Sin embargo, su segundo ejemplo es peor: está desreferenciando el puntero y haciendo una copia del objeto. De esta forma, perderás el puntero al objeto creado con new
, por lo que nunca podrás eliminarlo aunque lo desees.
Qué deberías hacer
Deberías preferir la duración de almacenamiento automático. Necesita un objeto nuevo, 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ámico, 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
Esta es una expresión común que utiliza el nombre RAII no muy descriptivo ( Inicialización de recursos es 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 sea que desee.
Este punto_automático ya 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_ptr
pero ahora está obsoleto porque tiene un extraño comportamiento de copia.
Y luego hay algunos ejemplos incluso más inteligentes, como std::shared_ptr
, que permite punteros múltiples al mismo objeto y solo lo limpia cuando se destruye el último puntero.
B object2 = *(new B());
Esta línea es la causa de la fuga. Vamos a elegir esto un poco ...
object2 es una variable de tipo B, almacenada en dicha dirección 1 (Sí, estoy escogiendo números arbitrarios aquí). En el lado derecho, ha solicitado una nueva B o un puntero a un objeto de tipo B. El programa se lo da gustosamente y 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 mediante el puntero en la dirección 3. A continuación, eliminó la referencia del puntero usando *
para obtener los datos a los que apunta el puntero (los datos en la dirección 2). Esto efectivamente crea una copia de esos datos y los asigna a object2, asignado en la dirección 1. Recuerde, es una COPIA, no la original.
Ahora, aquí está el problema:
¡Nunca almacenaste ese puntero en ningún lugar donde puedas usarlo! Una vez finalizada esta asignación, el puntero (memoria en la dirección 3, que usó 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 le queda es una copia de los datos de la dirección 2 en la dirección1. Dos de las mismas cosas sentadas en la memoria. Uno puede acceder, el otro no puede (porque perdió el camino hacia él). Es por eso que esto es una pérdida de memoria.
Te sugiero que, a partir de tu experiencia en C #, leas mucho sobre cómo funcionan los punteros en C ++. Son un tema avanzado y pueden tomarse un tiempo para comprenderlo, pero su uso será invaluable para usted.