C ++ delete – Exclui meus objects, mas ainda posso acessar os dados?

Eu escrevi um jogo de tetris simples e funcional com cada bloco como uma instância de um bloco único de class.

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); } 

Eu tenho uma function que procura por uma linha completa, e percorre a lista de blocos vinculados, excluindo os relevantes e reatribuindo os pointers -> next.

 SingleBlock *deleteBlock; SingleBlock *tempBlock; tempBlock = deleteBlock->next; delete deleteBlock; 

O jogo funciona, os blocos são excluídos corretamente e tudo funciona como deveria. No entanto, durante a inspeção, ainda posso acessar bits randoms de dados excluídos.

Se eu imprimir cada um dos valores de “x” de blocos únicos excluídos APÓS sua exclusão, alguns deles retornam lixo random (confirmando a exclusão) e alguns deles retornam 222, informando que mesmo que o destruidor tenha sido chamado os dados não foram realmente excluídos de o heap. Muitas tentativas idênticas mostram que são sempre os mesmos blocos específicos que não são excluídos corretamente.

Os 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 

Está sendo capaz de acessar dados além do esperado?

Desculpe se isso é um pouco longo.

Está sendo capaz de acessar dados além do esperado?

Isso é tecnicamente conhecido como comportamento indefinido. Não se surpreenda se lhe oferecer uma lata de cerveja.

Está sendo capaz de acessar dados além do esperado?

Na maioria dos casos, sim. A chamada de exclusão não zera a memory.

Observe que o comportamento não está definido. Usando certos compiladores, a memory pode ser zerada. Quando você chama delete, o que acontece é que a memory é marcada como disponível, então da próxima vez que alguém fizer uma nova , a memory pode ser usada.

Se você pensar sobre isso, é lógico – quando você diz ao compilador que você não está mais interessado na memory (usando delete ), por que o computador deveria gastar tempo com o zeramento.

É o que o C ++ chama de comportamento indefinido – talvez você possa acessar os dados, talvez não. Em qualquer caso, é a coisa errada a fazer.

Apagar não apaga nada – apenas marca a memory como “sendo livre para reutilização”. Até que alguma outra chamada de alocação reserve e preencha esse espaço, ela terá os dados antigos. No entanto, contando com isso é um grande não-não, basicamente, se você excluir algo esquecê-lo.

Uma das práticas a esse respeito que é frequentemente encontrada em bibliotecas é uma function Delete:

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

Isso nos impede de acessar acidentalmente a memory inválida.

Note que é perfeitamente correto chamar delete NULL; .

A memory do heap é como um monte de frameworks-negros. Imagine que você é um professor. Enquanto você está ensinando sua turma, o quadro negro pertence a você e você pode fazer o que quiser com ele. Você pode rabiscar e sobrescrever as coisas como quiser.

Quando a aula termina e você está prestes a sair da sala, não existe uma política que exija que você apague a lousa – você simplesmente entrega a lousa para a próxima professora que geralmente poderá ver o que você escreveu.

O sistema não limpa a memory quando você a libera via delete() . O conteúdo ainda está acessível até que a memory seja atribuída para reutilização e sobregravada.

delete desaloca a memory, mas não a modifica nem a zera. Ainda assim você não deve acessar a memory desalocada.

Após a exclusão de um object, não é definido o que acontecerá com o conteúdo da memory ocupada. Isso significa que essa memory está livre para ser reutilizada, mas a implementação não precisa sobrescrever os dados que estavam lá originalmente e não precisa reutilizar a memory imediatamente.

Você não deve acessar a memory depois que o object se foi, mas não deve ser surpreendente que alguns dados permaneçam intactos.

Não vai zerar / trocar memory ainda … mas em algum momento, o tapete vai ser puxado debaixo dos seus pés.

Não, certamente não é previsível: depende da rapidez com que a alocação / desalocação de memory é rápida.

Sim, isso pode ser esperado às vezes. Considerando que o new espaço de reserva para dados, delete simplesmente invalida um ponteiro criado com o new , permitindo que os dados sejam gravados nos locais anteriormente reservados; não elimina necessariamente os dados. No entanto, você não deve confiar nesse comportamento porque os dados nesses locais podem ser alterados a qualquer momento, possivelmente causando a má execução do programa. É por isso que depois de usar delete em um ponteiro (ou delete[] em um array alocado com new[] ), você deve atribuir NULL a ele para que você não possa adulterar um ponteiro inválido, supondo que você não irá alocar memory usando new ou new[] antes de usar esse ponteiro novamente.

Isso levará a um comportamento indefinido e excluirá a desalocação da memory; ela não reinicializará com zero.

Se você quiser zerar, faça:

 SingleBlock::~SingleBlock() { x = y = 0 ; } 

Embora seja possível que seu tempo de execução não relate esse erro, o uso de um tempo de execução de verificação de erros adequado, como o Valgrind, alertará você sobre o uso da memory após sua liberação.

Eu recomendo que, se você escrever código com pointers new / delete e raw (ao invés de std::make_shared() e similar), você exerça seus testes unitários sob o Valgrind para ao menos ter uma chance de detectar tais erros.

Bem, eu tenho pensado sobre isso por um bom tempo também, e eu tentei executar alguns testes para entender melhor o que está acontecendo sob o capô. A resposta padrão é que depois de você chamar delete, você não deve esperar que nada de bom acesse esse ponto de memory. No entanto, isso não parece suficiente para mim. O que está realmente acontecendo ao chamar delete (ptr) ? Aqui está o que eu encontrei. Estou usando o g ++ no Ubuntu 16.04, então isso pode ter um papel nos resultados.

O que eu esperava pela primeira vez ao usar o operador delete era que a memory liberada seria devolvida ao sistema para uso em outros processos. Deixe-me dizer que isso não acontece em nenhuma das circunstâncias que tentei.

A memory liberada com delete ainda parece ser alocada para o programa que primeiro alocou com o novo . Eu tentei, e não há diminuição de uso de memory depois de chamar excluir . Eu tinha um software que alocava cerca de 30MB de listas por meio de novas chamadas e, em seguida, as liberava com chamadas de exclusão subseqüentes. O que aconteceu é que, olhando para o monitor do sistema enquanto o programa estava em execução, mesmo um longo sono após as chamadas de exclusão , consumo de memory meu o programa era o mesmo. Nenhuma diminuição! Isso significa que a exclusão não libera memory para o sistema.

Na verdade, parece que a memory alocada por um programa é dele para sempre! No entanto, o ponto é que, se desalocada, a memory pode ser usada novamente pelo mesmo programa sem precisar alocar mais. Eu tentei alocar 15MB, liberando-os e depois alocando outros 15MB de dados depois, e o programa nunca usou 30MB. O monitor do sistema sempre mostrou cerca de 15MB. O que eu fiz, em relação ao teste anterior, foi apenas mudar a ordem em que as coisas aconteceram: metade da alocação, metade da desalocação, outra metade da alocação.

Assim, aparentemente , a memory usada por um programa pode aumentar, mas nunca diminuir . Eu pensei que talvez a memory fosse realmente liberada para outros processos em situações críticas, como quando não há mais memory disponível. Afinal, que sentido faria para que um programa guardasse sua própria memory para sempre, quando outros processos pedem isso? Então, eu aloquei os 30MB novamente e, ao desalocá-los, eu corro um memtester com o máximo de memory física que consegui. Eu esperava ver meu software distribuindo sua memory para o memtester. Mas acho que isso não aconteceu!

Eu fiz um pequeno screencast que mostra a coisa em ação:

Excluir exemplo de memória

Para ser 100% honesto, houve uma situação em que algo aconteceu. Quando tentei o memtester com mais do que a memory física disponível no meio do processo de desalocação do meu programa, a memory usada pelo meu programa caiu para cerca de 3MB. O processo memtester foi morto automaticamente, e o que aconteceu foi ainda mais surpreendente! O uso de memory do meu programa aumentou com cada chamada de exclusão! Foi como se o Ubuntu estivesse restaurando toda a sua memory após o incidente do memtester.

Extraído de http://www.thecrowned.org/c-delete-operator-really-frees-memory