2009-12-18 12 views
23

He escrito un juego de tetris simple y funcional con cada bloque como una instancia de un bloque individual de clase.C++ delete - ¿Elimina mis objetos pero todavía puedo acceder a los datos?

class SingleBlock 
{ 
    public: 
    SingleBlock(int, int); 
    ~SingleBlock(); 

    int x; 
    int y; 
    SingleBlock *next; 
}; 

class MultiBlock 
{ 
    public: 
    MultiBlock(int, int); 

    SingleBlock *c, *d, *e, *f; 
}; 

SingleBlock::SingleBlock(int a, int b) 
{ 
    x = a; 
    y = b; 
} 

SingleBlock::~SingleBlock() 
{ 
    x = 222; 
} 

MultiBlock::MultiBlock(int a, int b) 
{ 
    c = new SingleBlock (a,b); 
    d = c->next = new SingleBlock (a+10,b); 
    e = d->next = new SingleBlock (a+20,b); 
    f = e->next = new SingleBlock (a+30,b); 
} 

que tienen una función que analiza en busca de una línea completa, y se ejecuta a través de la lista enlazada de bloques de eliminación de los relevantes y la reasignación de los -> Siguiente punteros.

SingleBlock *deleteBlock; 
SingleBlock *tempBlock; 

tempBlock = deleteBlock->next; 
delete deleteBlock; 

El juego funciona, los bloques se eliminan correctamente y todo funciona como se supone que debe. Sin embargo, en la inspección, todavía puedo acceder a los bits aleatorios de los datos eliminados.

Si imprimo cada uno de los valores eliminados de los bloques individuales "x" DESPUÉS de su eliminación, algunos devuelven basura aleatoria (confirmando la eliminación) y algunos devuelven 222, diciéndome que aunque el destructor se llamó datos wasn ' t en realidad eliminado del montón. Muchos ensayos idénticos muestran que siempre son los mismos bloques específicos que no se eliminan correctamente.

Los resultados:

Existing Blocks: 
Block: 00E927A8 
Block: 00E94290 
Block: 00E942B0 
Block: 00E942D0 
Block: 00E942F0 
Block: 00E94500 
Block: 00E94520 
Block: 00E94540 
Block: 00E94560 
Block: 00E945B0 
Block: 00E945D0 
Block: 00E945F0 
Block: 00E94610 
Block: 00E94660 
Block: 00E94680 
Block: 00E946A0 

Deleting Blocks: 
Deleting ... 00E942B0, X = 15288000 
Deleting ... 00E942D0, X = 15286960 
Deleting ... 00E94520, X = 15286992 
Deleting ... 00E94540, X = 15270296 
Deleting ... 00E94560, X = 222 
Deleting ... 00E945D0, X = 15270296 
Deleting ... 00E945F0, X = 222 
Deleting ... 00E94610, X = 222 
Deleting ... 00E94660, X = 15270296 
Deleting ... 00E94680, X = 222 

es ser capaz de acceder a los datos desde el más allá de esperar?

Lo siento si esto es un poco largo aliento.

+1

La política más segura es eliminar un elemento cuando ya no se utiliza, y nunca se refieren a ella de nuevo. Los Smart Pointers pueden ayudar cuando más de un puntero se refiere al mismo objeto en la memoria. –

+0

Si puede acceder a los bloques, puede volver a eliminarlos. Eso es malo. No lo hagas –

+15

A veces creo que una palabra clave mejor que 'borrar 'hubiera sido' olvidar'; en realidad no le está diciendo al compilador que * elimine * nada tanto como * deje de preocuparse por * it (y deje que otra persona haga lo que quiera con i) como devolver un libro a la biblioteca en lugar de quemarlo. –

Respuesta

69

¿Se puede tener acceso a datos desde más allá de la tumba que se esperan?

Esto se conoce técnicamente como Comportamiento indefinido. No se sorprenda si le ofrece una lata de cerveza tampoco.

+9

Además, es bueno agregar el corolario de ese hecho ... Si uno tenía datos "sensibles" almacenados en la memoria, se debe considerar que es una buena práctica sobrescribirlo por completo antes de eliminarlo (para evitar otros segmentos de código de acceder a él). – Romain

+0

Eso debe ser manejado antes de la llamada dtor. – dirkgently

+1

... o al menos en el dtor. – dirkgently

0

No se pondrá a cero/cambiará la memoria todavía ... pero en algún punto, la alfombra se va a sacar de debajo de los pies.

No, ciertamente no es predecible: depende de qué tan rápido se agita la asignación/desasignación de memoria.

28

¿Se puede acceder a los datos desde más allá de la tumba que se espera?

En la mayoría de los casos, sí. Llamar a eliminar no pone a cero la memoria.

Tenga en cuenta que el comportamiento no está definido. Usando ciertos compiladores, la memoria puede ponerse a cero. Cuando llama a eliminar, lo que sucede es que la memoria está marcada como disponible, por lo que la próxima vez que alguien haga nuevo, se puede usar la memoria.

Si lo piensas bien, es lógico: cuando le dices al compilador que ya no estás interesado en la memoria (usando delete), ¿por qué debería la computadora dedicar tiempo a ponerlo en cero?

+0

Sin embargo, no hay garantía de que 'new' o' malloc' no asigne algunos objetos nuevos encima de los antiguos. Otro desastre puede ser el recolector de basura del sistema. Además, si su programa recibe memoria de un grupo de memoria de todo el sistema, otros programas pueden escribir sobre los datos fantasmas. –

+0

En realidad, no. El acceso correcto a la memoria eliminada no es el comportamiento esperado, es un comportamiento indefinido. Otra asignación podría sobrescribir fácilmente la memoria que acaba de liberar. –

+4

@Thomas Matthews No digo que sea una buena idea intentar acceder a él. @Curt Nichols Eso está jugando con las palabras. Dependiendo de qué compilador use, puede * esperar * que la memoria no se ponga a cero inmediatamente cuando se llame a delete. Obviamente, no puedes estar seguro de eso. – Martin

1

delete desasigna la memoria, pero no la modifica ni la pone a cero. Aún así, no debería acceder a la memoria desasignada.

9

Es lo que C++ llama comportamiento indefinido: es posible que pueda acceder a los datos, es posible que no. En cualquier caso, es lo incorrecto de hacer.

2

El sistema no borra la memoria cuando la suelta a través de delete(). Por lo tanto, todavía se puede acceder al contenido hasta que la memoria se haya asignado para su reutilización y se haya sobrescrito.

1

Después de eliminar un objeto, no está definido qué sucederá con el contenido de la memoria que ocupó. Significa que la memoria puede reutilizarse libremente, pero la implementación no tiene que sobrescribir los datos que estaban allí originalmente y no tiene que reutilizar la memoria inmediatamente.

No debe acceder a la memoria después de que el objeto se haya ido, pero no debe sorprender que algunos datos permanezcan intactos allí.

0

Sí, a veces se puede esperar. Mientras que new reserva espacio para los datos, delete simplemente invalida un puntero creado con new, permitiendo que los datos se escriban en las ubicaciones previamente reservadas; no necesariamente borra los datos. Sin embargo, no debe confiar en ese comportamiento porque los datos en esas ubicaciones podrían cambiar en cualquier momento, posiblemente causando que su programa se comporte mal. Es por eso que después de usar delete en un puntero (o delete[] en una matriz asignada con new[]), debe asignarle NULL para que no pueda manipular un puntero no válido, suponiendo que no va a asignar memoria usando new o new[] antes de usar ese puntero nuevamente

+3

No hay nada en el estándar de lenguaje C++ que impida que' delete' borre la memoria que se ha eliminado o llenado con un valor extraño. Es implementación definida. –

8

Eliminar no elimina nada, simplemente marca la memoria como "libre para reutilizar". Hasta que otras asignaciones de llamadas reserven y llenen ese espacio, tendrán los datos anteriores. Sin embargo, confiar en eso es un gran no-no, básicamente si borras algo, olvídalo.

Una de las prácticas en este sentido, que se encuentra a menudo en las bibliotecas es una función de borrado:

template< class T > void Delete(T*& pointer) 
{ 
    delete pointer; 
    pointer = NULL; 
} 

Esto nos impide acceder accidentalmente memoria no válida.

Tenga en cuenta que está perfectamente bien llamar al delete NULL;.

+0

Incluso si no usa una macro, es una buena práctica establecer un puntero a NULL inmediatamente después de liberarlo. Es un buen hábito para entrar, evitando este tipo de malentendidos. –

+1

@Kornel Cualquier biblioteca C++ que utilizara tal macro sería extremadamente sospechosa, en mi humilde opinión. En el muy leasy, debe ser una función de plantilla en línea. –

+8

@Mark Configurar punteros a NULL después de eliminar no es una buena práctica universal en C++. Hay ocasiones en las que es bueno hacerlo, y momentos en los que no tiene sentido y puede ocultar errores. –

1

Conducirá a un comportamiento indefinido y eliminará la memoria de desasignación, no la reinicializa con cero.

Si usted quiere que sea puesta a cero y luego hacer:

SingleBlock::~SingleBlock() 

{ x = y = 0 ; } 
3

memoria de almacenamiento dinámico es como un montón de pizarras. Imagina que eres un maestro. Mientras enseñas a tu clase, la pizarra te pertenece y puedes hacer lo que quieras con ella. Puedes garabatear y sobrescribir cosas como desees.

Cuando termine la clase y esté a punto de abandonar la sala, no existe una política que exija que borre la pizarra; simplemente le entrega la pizarra al siguiente profesor que generalmente podrá ver lo que usted anoto.

+1

Si un compilador puede determinar que el código inevitablemente va a acceder (incluso mirar) a una parte de la pizarra que no posee, dicha determinación liberará al compilador de las leyes del tiempo y la causalidad; algunos compiladores lo explotan de una forma que hubiera sido considerada absurda hace una década (muchas de las cuales todavía son absurdas, en mi humilde opinión). Podría entender que si dos piezas de código no dependen el uno del otro, un compilador puede intercalar su procesamiento de cualquier forma, incluso si eso hace que UB golpee "temprano", pero una vez que UB se vuelve inevitable, todas las reglas salen volando por la ventana. – supercat

1

Aunque es posible que su tiempo de ejecución no reporte este error, el uso de un tiempo de ejecución de comprobación de errores adecuado como Valgrind lo alertará sobre el uso de la memoria después de que se haya liberado.

recomiendo que si se escribe código con new/delete y punteros primas (en lugar de std::make_shared() y similares), que haga ejercicio pruebas unitarias bajo Valgrind por lo menos tener la oportunidad de detectar este tipo de errores.

1

Bueno, me he estado preguntando sobre esto durante bastante tiempo también, y he intentado hacer algunas pruebas para comprender mejor lo que está sucediendo bajo el capó. La respuesta estándar es que después de llamar al eliminar, no debe esperar que nada bueno acceda a ese punto de memoria. Sin embargo, esto no me pareció suficiente. ¿Qué está sucediendo realmente al llamar a delete (ptr)? Esto es lo que he encontrado. Estoy usando g ++ en Ubuntu 16.04, así que esto puede desempeñar un papel en los resultados.

Lo que primero esperaba al utilizar el operador de eliminación era que la memoria liberada se devolvía al sistema para su uso en otros procesos. Déjame decir esto no ocurre bajo ninguna de las circunstancias que he intentado.

memoria en libertad con eliminar todavía parece estar asignado al programa se asigna en primer lugar con nueva. Lo he intentado, y no hay una disminución en el uso de la memoria después de llamar al eliminar. Tenía un software que asignaba alrededor de 30MB de listas a través de llamadas nuevas, y luego las liberaba con las siguientes llamadas de eliminación. Lo que sucedió es que, mirando el monitor del sistema mientras el programa se estaba ejecutando, incluso un largo sueño después de borre las llamadas, el consumo de memoria el programa era el mismo. ¡Sin disminución! Esto significa que eliminar no libera memoria en el sistema.

De hecho, parece que la memoria asignada por un programa es suya para siempre. Sin embargo, el punto es que, si se desasigna, la memoria puede ser utilizada nuevamente por el mismo programa sin tener que asignar más. Traté de asignar 15MB, liberarlos, y luego asignar otros 15MB de datos después, y el programa nunca usó 30MB. El monitor del sistema siempre mostró alrededor de 15MB. Lo que hice, con respecto a la prueba anterior, fue simplemente cambiar el orden en que sucedieron las cosas: la mitad de la asignación, la mitad de la asignación, la otra mitad de la asignación.

Por lo tanto, aparentemente la memoria utilizada por un programa puede aumentar, pero nunca se reduce. Pensé que tal vez la memoria realmente se liberaría para otros procesos en situaciones críticas, como cuando no hay más memoria disponible. Después de todo, ¿qué sentido tendría dejar que un programa guarde su propia memoria para siempre, cuando otros procesos lo piden? Así que asigné los 30 MB de nuevo y mientras los deslocalé Ejecuto un memtester con tanta memoria física como pude. Esperaba ver mi software repartir su memoria a memtester. Pero adivinen, ¡no sucedió!

He hecho una breve screencast que muestra la hora de la acción:

Delete example memory

para ser 100% honesto, hubo una situación en la que algo sucedió. Cuando probé memtester con más de la memoria física disponible en el medio del proceso de desasignación de mi programa, la memoria utilizada por mi programa cayó a alrededor de 3MB. Sin embargo, el proceso de memtester se eliminó automáticamente, ¡y lo que sucedió fue aún más sorprendente! ¡El uso de memoria de mi programa aumentó con cada llamada de eliminación! Era como si Ubuntu estuviera restaurando toda su memoria después del incidente del memtester.

Tomado de http://www.thecrowned.org/c-delete-operator-really-frees-memory

Cuestiones relacionadas