Vou começar dizendo: use ponteiros inteligentes e você nunca precisará se preocupar com isso.
Quais são os problemas com o código a seguir?
Foo * p = new Foo;
// (use p)
delete p;
p = NULL;
Isso foi desencadeado por uma resposta e comentários para outra pergunta. Um comentário de Neil Butterworth gerou alguns votos positivos:
Definir ponteiros como NULL após a exclusão não é uma boa prática universal em C ++. Há momentos em que é uma coisa boa a fazer, e momentos em que é inútil e pode ocultar erros.
Existem muitas circunstâncias em que isso não ajudaria. Mas na minha experiência, não pode doer. Alguém me esclarece.
c++
pointers
null
dynamic-allocation
Mark Ransom
fonte
fonte
delete
Porém, é seguro para um ponteiro nulo, que é uma das razões pelas quais zerar um ponteiro pode ser bom.Respostas:
Definir um ponteiro como 0 (que é "nulo" no C ++ padrão, a definição NULL de C é um pouco diferente) evita falhas nas exclusões duplas.
Considere o seguinte:
Enquanto que:
Em outras palavras, se você não definir ponteiros excluídos para 0, terá problemas se estiver fazendo exclusões duplas. Um argumento contra a definição de ponteiros para 0 após a exclusão seria que, ao fazer isso, oculte os bugs de exclusão dupla e os deixe sem tratamento.
É melhor não ter bugs de exclusão dupla, obviamente, mas dependendo da semântica de propriedade e dos ciclos de vida do objeto, isso pode ser difícil na prática. Eu prefiro um bug de exclusão dupla mascarado sobre o UB.
Por fim, uma nota explicativa sobre o gerenciamento da alocação de objetos, sugiro que você analise a
std::unique_ptr
propriedade estrita / singular,std::shared_ptr
a propriedade compartilhada ou outra implementação de ponteiro inteligente, dependendo de suas necessidades.fonte
NULL
pode mascarar um bug de exclusão dupla. (Alguns podem considerar que essa máscara é realmente uma solução - é, mas não é muito boa, pois não chega à raiz do problema.) Mas não a define para máscaras NULL até agora (MUITO!) problemas comuns de acesso aos dados após a exclusão.unique_ptr
, que faz o queauto_ptr
tentou fazer, pela semântica de movimentos.Definir ponteiros como NULL depois de excluir o que apontou certamente não pode prejudicar, mas geralmente é um pouco de ajuda para um problema mais fundamental: por que você está usando um ponteiro? Eu posso ver dois motivos típicos:
std::vector
funciona, e resolve o problema de deixar acidentalmente ponteiros para a memória desalocada. Não há ponteiros.new
pode não ser o mesmo que odelete
chamado. Vários objetos podem ter usado o objeto simultaneamente nesse meio tempo. Nesse caso, um ponteiro compartilhado ou algo semelhante teria sido preferível.Minha regra geral é que, se você deixar os ponteiros no código do usuário, estará fazendo algo errado. O ponteiro não deve estar lá para apontar para o lixo em primeiro lugar. Por que não há um objeto que se responsabilize por garantir sua validade? Por que seu escopo não termina quando o objeto apontado termina?
fonte
new
.Eu tenho uma prática recomendada ainda melhor: sempre que possível, encerre o escopo da variável!
fonte
Eu sempre defino um ponteiro para
NULL
(agoranullptr
) após excluir o (s) objeto (s) para o qual ele aponta.Isso pode ajudar a capturar muitas referências à memória liberada (supondo que a plataforma falhe com um deref de um ponteiro nulo).
Ele não captura todas as referências à memória liberada se, por exemplo, você tiver cópias do ponteiro por aí. Mas alguns são melhores que nada.
Ele irá mascarar uma exclusão dupla, mas acho que essas são muito menos comuns do que acessos à memória já liberada.
Em muitos casos, o compilador irá otimizá-lo. Portanto, o argumento de que é desnecessário não me convence.
Se você já está usando RAII, não há muitos
delete
s no seu código, portanto, o argumento de que a atribuição extra causa confusão não me convence.Muitas vezes, é conveniente, ao depurar, ver o valor nulo em vez de um ponteiro obsoleto.
Se isso ainda o incomoda, use um ponteiro inteligente ou uma referência.
Também defino outros tipos de identificadores de recursos para o valor no-resource quando o recurso é liberado (o que normalmente ocorre apenas no destruidor de um wrapper RAII gravado para encapsular o recurso).
Trabalhei em um grande produto comercial (9 milhões de declarações) (principalmente em C). Em um ponto, usamos a macro mágica para anular o ponteiro quando a memória foi liberada. Isso imediatamente expôs muitos erros ocultos que foram rapidamente corrigidos. Tanto quanto me lembro, nunca tivemos um bug duplo-livre.
Atualização: a Microsoft acredita que é uma boa prática de segurança e recomenda a prática em suas políticas SDL. Aparentemente, o MSVC ++ 11 impedirá o ponteiro excluído automaticamente (em várias circunstâncias) se você compilar com a opção / SDL.
fonte
Em primeiro lugar, existem muitas perguntas sobre este assunto e tópicos relacionados, por exemplo Por que não excluir definir o ponteiro para NULL? .
No seu código, o problema que se passa em (use p). Por exemplo, se em algum lugar você tiver um código como este:
então, definir p como NULL realiza muito pouco, pois você ainda precisa se preocupar com o ponteiro p2.
Isso não quer dizer que definir um ponteiro como NULL seja sempre inútil. Por exemplo, se p fosse uma variável de membro que aponta para um recurso cuja vida útil não seja exatamente igual à classe que contém p, definir p como NULL pode ser uma maneira útil de indicar a presença ou ausência do recurso.
fonte
Se houver mais código após o
delete
, Sim. Quando o ponteiro é excluído em um construtor ou no final do método ou função, No.O objetivo desta parábola é lembrar ao programador, durante o tempo de execução, que o objeto já foi excluído.
Uma prática ainda melhor é usar ponteiros inteligentes (compartilhados ou com escopo) que excluem automaticamente seus objetos de destino.
fonte
Como outros já disseram,
delete ptr; ptr = 0;
não fará com que demônios voem do seu nariz. No entanto, incentiva o uso deptr
como uma bandeira dos tipos. O código fica cheiodelete
e definindo o ponteiro paraNULL
. A próxima etapa é espalharif (arg == NULL) return;
seu código para se proteger contra o uso acidental de umNULL
ponteiro. O problema ocorre quando as verificaçõesNULL
se tornam seu principal meio de verificar o estado de um objeto ou programa.Tenho certeza de que há um cheiro de código sobre o uso de um ponteiro como sinalizador em algum lugar, mas não encontrei nenhum.
fonte
NULL
não for um valor válido, provavelmente deverá usar uma referência.Vou mudar sua pergunta um pouco:
Há dois cenários em que a configuração do ponteiro como NULL pode ser ignorada:
Enquanto isso, argumentar que definir o ponteiro como NULL pode ocultar erros parece-me argumentar que você não deve corrigir um erro, porque a correção pode ocultar outro erro. Os únicos erros que podem aparecer se o ponteiro não estiver definido como NULL seriam os que tentarem usar o ponteiro. Mas defini-lo como NULL realmente causaria exatamente o mesmo bug que seria mostrado se você o usasse com memória liberada, não faria?
fonte
Se você não tiver nenhuma outra restrição que o force a definir ou não definir o ponteiro para NULL depois de excluí-lo (uma dessas restrições foi mencionada por Neil Butterworth ), minha preferência pessoal é deixar como está.
Para mim, a pergunta não é "é uma boa ideia?" mas "que comportamento eu impediria ou permitiria ter sucesso fazendo isso?" Por exemplo, se isso permite que outro código veja que o ponteiro não está mais disponível, por que outro código está tentando procurar ponteiros liberados depois que eles são liberados? Geralmente, é um bug.
Ele também faz mais trabalho do que o necessário, além de dificultar a depuração post-mortem. Quanto menos você tocar na memória depois de não precisar dela, mais fácil será descobrir por que algo travou. Muitas vezes, confiei no fato de que a memória está em um estado semelhante ao quando ocorreu um bug específico para diagnosticar e corrigir esse bug.
fonte
A nulidade explícita após a exclusão sugere fortemente ao leitor que o ponteiro representa algo que é conceitualmente opcional . Se eu visse isso sendo feito, começaria a me preocupar com o fato de que em qualquer lugar da fonte o ponteiro seja usado para que seja testado primeiro contra NULL.
Se é isso que você realmente quer dizer, é melhor deixar isso explícito na fonte usando algo como boost :: optional
Mas se você realmente quer que as pessoas saibam que o ponteiro "deu errado", vou concordar 100% com aqueles que dizem que a melhor coisa a fazer é fazê-lo sair do escopo. Então você está usando o compilador para evitar a possibilidade de desreferências ruins no tempo de execução.
Esse é o bebê em toda a água do banho em C ++, não deve descartá-lo. :)
fonte
Em um programa bem estruturado com verificação de erro apropriada, não há razão para não atribuir nulo.
0
permanece sozinho como um valor inválido universalmente reconhecido neste contexto. Falhar muito e falhar em breve.Muitos dos argumentos contra a atribuição
0
sugerem que ele poderia ocultar um bug ou complicar o fluxo de controle. Fundamentalmente, isso é um erro upstream (não sua culpa (desculpe o trocadilho)) ou outro erro em nome do programador - talvez até uma indicação de que o fluxo do programa tenha se tornado muito complexo.Se o programador deseja introduzir o uso de um ponteiro que pode ser nulo como um valor especial e escrever todos os desvios necessários, é uma complicação que eles introduziram deliberadamente. Quanto melhor a quarentena, mais cedo você encontra casos de uso indevido e menos eles conseguem se espalhar para outros programas.
Programas bem estruturados podem ser projetados usando recursos do C ++ para evitar esses casos. Você pode usar referências ou apenas dizer que "passar / usar argumentos nulos ou inválidos é um erro" - uma abordagem igualmente aplicável a contêineres, como ponteiros inteligentes. O aumento do comportamento consistente e correto proíbe que esses erros cheguem longe.
A partir daí, você tem apenas um escopo e contexto muito limitados, onde um ponteiro nulo pode existir (ou é permitido).
O mesmo pode ser aplicado aos ponteiros que não são
const
. Seguir o valor de um ponteiro é trivial porque seu escopo é muito pequeno e o uso inadequado é verificado e bem definido. Se o seu conjunto de ferramentas e engenheiros não puderem seguir o programa após uma leitura rápida ou se houver uma verificação de erro inadequada ou um fluxo inconsistente / branda do programa, você terá outros problemas maiores.Por fim, seu compilador e seu ambiente provavelmente possuem algumas proteções para os momentos em que você deseja introduzir erros (rabiscar), detectar acessos à memória liberada e capturar outros UB relacionados. Você também pode introduzir diagnósticos semelhantes em seus programas, geralmente sem afetar os programas existentes.
fonte
Deixe-me expandir o que você já colocou em sua pergunta.
Aqui está o que você colocou em sua pergunta, em forma de marcador:
Definir ponteiros como NULL após a exclusão não é uma boa prática universal em C ++. Há momentos em que:
No entanto, não há momentos em que isso seja ruim ! Você não introduzir mais erros por anulando-lo explicitamente, você não vai vazar memória, você não vai causar um comportamento indefinido para acontecer.
Portanto, em caso de dúvida, anule-o.
Dito isto, se você sentir que precisa anular explicitamente algum ponteiro, parece-me que você não dividiu um método o suficiente e deve considerar a abordagem de refatoração chamada "Método de extração" para dividir o método em partes separadas.
fonte
Sim.
O único "dano" que isso pode causar é introduzir ineficiência (uma operação desnecessária de armazenamento) em seu programa - mas essa sobrecarga será insignificante em relação ao custo de alocar e liberar o bloco de memória na maioria dos casos.
Se você não fizer isso, terá alguns erros desagradáveis de apontador um dia.
Eu sempre uso uma macro para excluir:
(e semelhante para uma matriz, livre (), liberando identificadores)
Você também pode escrever métodos de "exclusão automática" que fazem referência ao ponteiro do código de chamada, para forçar o ponteiro do código de chamada para NULL. Por exemplo, para excluir uma subárvore de muitos objetos:
editar
Sim, essas técnicas violam algumas regras sobre o uso de macros (e sim, hoje em dia você provavelmente poderia obter o mesmo resultado com modelos) - mas, ao longo de muitos anos, nunca acessei a memória morta - uma das mais desagradáveis e mais difíceis. consome mais tempo para depurar problemas que você pode enfrentar. Na prática, durante muitos anos, eles efetivamente eliminaram uma classe de bugs de todos os times em que os introduzi.
Também há várias maneiras de implementar o acima - estou apenas tentando ilustrar a idéia de forçar as pessoas a NULL um ponteiro se elas excluírem um objeto, em vez de fornecer um meio para liberar a memória que não NULL o ponteiro do chamador .
Obviamente, o exemplo acima é apenas um passo em direção a um ponteiro automático. O que eu não sugeri porque o OP estava perguntando especificamente sobre o caso de não usar um ponteiro automático.
fonte
anObject->Delete(anObject)
invalidar oanObject
ponteiro. Isso é assustador. Você deve criar um método estático para isso, para que seja forçado a fazer,TreeItem::Delete(anObject)
no mínimo."Há momentos em que é uma coisa boa a fazer, e momentos em que é inútil e pode ocultar erros"
Eu posso ver dois problemas: Esse código simples:
torna-se para um for-liner em ambiente multithread:
As "melhores práticas" de Don Neufeld nem sempre se aplicam. Por exemplo, em um projeto automotivo, tivemos que definir ponteiros para 0, mesmo em destruidores. Eu posso imaginar no software de segurança crítica que essas regras não são incomuns. É mais fácil (e sábio) segui-los do que tentar persuadir o time / verificador de código para cada ponteiro usado no código, que uma linha que anula esse ponteiro é redundante.
Outro perigo é confiar nessa técnica no código de exceções:
Nesse código, você produz vazamento de recursos e adia o problema ou o processo falha.
Portanto, esses dois problemas que passam espontaneamente pela minha cabeça (Herb Sutter certamente diria mais) me fazem todas as perguntas do tipo "Como evitar o uso de ponteiros inteligentes e fazer o trabalho com segurança com os ponteiros normais" como obsoletos.
fonte
Sempre há Dangling Pointers para se preocupar.
fonte
Se você deseja realocar o ponteiro antes de usá-lo novamente (desreferenciando-o, passando-o para uma função etc.), tornar o ponteiro NULL é apenas uma operação extra. No entanto, se você não tiver certeza se será realocado ou não antes de ser usado novamente, defini-lo como NULL é uma boa idéia.
Como muitos já disseram, é claro que é muito mais fácil usar apenas ponteiros inteligentes.
Edit: Como Thomas Matthews disse nesta resposta anterior , se um ponteiro é excluído em um destruidor, não há necessidade de atribuir NULL a ele, pois ele não será usado novamente porque o objeto já está sendo destruído.
fonte
Eu posso imaginar definir um ponteiro para NULL depois de excluí-lo, sendo útil em casos raros, em que há um cenário legítimo de reutilizá-lo em uma única função (ou objeto). Caso contrário, não faz sentido - um ponteiro precisa apontar para algo significativo enquanto existir - ponto final.
fonte
Se o código não pertencer à parte mais crítica de desempenho do seu aplicativo, mantenha-o simples e use um shared_ptr:
Ele realiza a contagem de referência e é seguro para threads. Você pode encontrá-lo no espaço de nomes tr1 (std :: tr1, #include <memory>) ou, se o seu compilador não fornecer, obtenha-o do impulso.
fonte