Pergunta

Vou começar por dizer, o uso de ponteiros inteligentes e você nunca terá que 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;

Este foi provocada por uma resposta e comentários a outra pergunta.Um comentário Neil Butterworth gerou um pouco upvotes:

Definição de ponteiros para NULL seguinte excluir não é universal boas práticas em C++.Há momentos em que é uma boa coisa a fazer, e momentos em que é inútil e pode esconder erros.

Há muitas circunstâncias em que ele não iria ajudar.Mas, na minha experiência, ele não pode machucar.Alguém me esclarecer.

Foi útil?

Solução

Definir um ponteiro para 0 (que é "nulo" no C ++ padrão, o NULL define de C é um pouco diferente) evita falhas em exclusão dupla.

Considere o seguinte:

Foo* foo = 0; // Sets the pointer to 0 (C++ NULL)
delete foo; // Won't do anything

Enquanto:

Foo* foo = new Foo();
delete foo; // Deletes the object
delete foo; // Undefined behavior 

Em outras palavras, se você não definir ponteiros excluídos para 0, terá problemas se estiver fazendo exclusão dupla. Um argumento contra a definição de ponteiros para 0 após a exclusão seria que isso apenas máscara bugs exclua duplo e os deixa iluminados.

É 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 de alcançar na prática. Eu prefiro um bug duplo de exclusão mascarado sobre o ub.

Finalmente, uma boneco sidra sobre gerenciamento de alocação de objetos, sugiro que você dê uma olhada std::unique_ptr Para propriedade rigorosa/singular, std::shared_ptr Para propriedade compartilhada ou outra implementação de ponteiro inteligente, dependendo de suas necessidades.

Outras dicas

Definir ponteiros para nulos depois de excluir o que apontou certamente não pode machucar, mas geralmente é um pouco de band-aid em relação a um problema mais fundamental: por que você está usando um ponteiro em primeiro lugar? Eu posso ver dois motivos típicos:

  • Você simplesmente queria algo alocado na pilha. Nesse caso, envolvê -lo em um objeto RAII teria sido muito mais seguro e limpo. Termine o escopo do objeto RAII quando você não precisar mais do objeto. É assim que std::vector Trabalha, e resolve o problema de deixar acidentalmente os ponteiros para a memória desalocada. Não há dicas.
  • Ou talvez você quisesse alguma semântica complexa de propriedade compartilhada. O ponteiro voltou de new pode não ser o mesmo que aquele que delete é chamado. Vários objetos podem ter usado o objeto simultaneamente enquanto isso. Nesse caso, um ponteiro compartilhado ou algo semelhante teria sido preferível.

Minha regra geral é que, se você deixar ponteiros no código do usuário, estará fazendo errado. O ponteiro não deveria estar lá para apontar para lixo em primeiro lugar. Por que não existe um objeto assumindo a responsabilidade de garantir sua validade? Por que o seu escopo não termina quando o objeto apontado?

Eu tenho uma prática recomendada ainda melhor: sempre que possível, encerre o escopo da variável!

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}

Eu sempre defino um ponteiro para NULL (agora nullptr) Depois de excluir o (s) objeto (s), ele aponta.

  1. Pode ajudar a capturar muitas referências à memória liberada (assumindo que sua plataforma falhas em um deref de um ponteiro nulo).

  2. Ele não captará todas as referências à memória gratuita se, por exemplo, você tiver cópias do ponteiro por aí. Mas alguns são melhores que nenhum.

  3. Ele mascarará um contrato duplo, mas acho que são muito menos comuns do que os acessos à memória já liberada.

  4. Em muitos casos, o compilador o otimiza. Portanto, o argumento de que é desnecessário não me convence.

  5. Se você já está usando RAII, não há muitos deletes em seu código, para começar, de modo que o argumento de que a tarefa extra causa desordem não me convence.

  6. Muitas vezes, é conveniente, ao depurar, ver o valor nulo, em vez de um ponteiro obsoleto.

  7. Se isso ainda o incomoda, use um ponteiro inteligente ou uma referência.

Eu também defino outros tipos de alças de recursos para o valor de não recursos quando o recurso é gratuito (que normalmente está apenas no destruidor de um invólucro RAII escrito para encapsular o recurso).

Eu trabalhei em um grande produto comercial (9 milhões de declarações) (principalmente em C). A certa altura, usamos a macro magia para anular o ponteiro quando a memória foi libertada. Isso imediatamente expôs muitos bugs espreitados que foram prontamente corrigidos. Tanto quanto me lembro, nunca tivemos um bug duplo.

Atualizar: A Microsoft acredita que é uma boa prática para a segurança e recomenda a prática em suas políticas de SDL. Aparentemente msvc ++ 11 irá pique o ponteiro excluído automaticamente (em muitas circunstâncias) se você compilar com a opção /sdl.

Em primeiro lugar, existem muitas perguntas existentes sobre isso e tópicos intimamente relacionados, por exemplo Por que excluir não define o ponteiro para NULL?.

No seu código, o problema do que acontece (use p). Por exemplo, se algum lugar você tiver código como este:

Foo * p2 = p;

Em seguida, definir P como NULL realiza muito pouco, pois você ainda tem o ponteiro P2 para se preocupar.

Isso não quer dizer que definir um ponteiro para NULL seja sempre inútil. Por exemplo, se p fosse uma variável de membro apontando para um recurso que a vida útil não era exatamente a mesma que a classe que continha P, definir p para nulo pode ser uma maneira útil de indicar a presença ou ausência do recurso.

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, não

O objetivo desta parábola é lembrar o programador, durante o tempo de execução, que o objeto já foi excluído.

Uma prática ainda melhor é usar ponteiros inteligentes (compartilhados ou escopo) que excluem automaticamente seus objetos de destino.

Como outros disseram, delete ptr; ptr = 0; não vai fazer com que os demônios voem para fora do seu nariz. No entanto, incentiva o uso de ptr como uma espécie de bandeira. O código fica cheio de delete e definir o ponteiro para NULL. O próximo passo é espalhar if (arg == NULL) return; através do seu código para proteger contra o uso acidental de um NULL ponteiro. O problema ocorre uma vez que os cheques NULL Torne -se seu principal meio de verificar o estado de um objeto ou programa.

Tenho certeza de que há um cheiro de código em usar um ponteiro como bandeira em algum lugar, mas não encontrei uma.

Vou mudar sua pergunta um pouco:

Você usaria um ponteiro não inicializado? Você sabe, um que você não definiu para nulo ou alocar a memória para a qual aponta?

Existem dois cenários em que definir o ponteiro para NULL pode ser ignorado:

  • A variável de ponteiro sai do escopo imediatamente
  • Você sobrecarregou a semântica do ponteiro e está usando seu valor não apenas como ponteiro de memória, mas também como um valor de chave ou bruto. Essa abordagem, no entanto, sofre de outros problemas.

Enquanto isso, argumentando que definir o ponteiro para NULL pode ocultar erros para mim parece argumentar que você não deve corrigir um bug, porque a correção pode ocultar outro bug. Os únicos bugs que podem mostrar se o ponteiro não estiver definido como nulo seriam os que tentam usar o ponteiro. Mas configurá -lo como NULL realmente causaria exatamente o mesmo bug que mostraria se você o usasse com memória liberada, não seria?

Se você não tem outra restrição que o obrigue a definir ou não definir o ponteiro para NULL depois de excluí -lo (uma dessas restrições foi mencionada por Neil Butterworth), então minha preferência pessoal é deixar isso.

Para mim, a pergunta não é "essa é uma boa ideia?" Mas "que comportamento eu impediria ou permitiria ter sucesso fazendo isso?" Por exemplo, se isso permitir que outro código veja que o ponteiro não está mais disponível, por que outro código está tentando olhar para os ponteiros liberados depois de serem libertados? Geralmente, é um bug.

Também faz mais trabalho do que o necessário, além de dificultar a depuração post mortem. Quanto menos você toca a memória depois de não precisar, mais fácil é descobrir por que algo travou. Muitas vezes, confiei no fato de que a memória está em um estado semelhante ao que ocorreu um bug específico para diagnosticar e corrigir o referido bug.

Explicitamente anular depois de excluir fortemente sugere a um leitor que o ponteiro representa algo que é conceitualmente opcional. Se eu visse isso sendo feito, começaria a me preocupar que em todos os lugares da fonte o ponteiro seja usado, ele deve ser testado pela primeira vez contra o NULL.

Se é isso que você realmente quer dizer, é melhor tornar isso explícito na fonte usando algo como Boost :: Opcional

optional<Foo*> p (new Foo);
// (use p.get(), but must test p for truth first!...)
delete p.get();
p = optional<Foo*>();

Mas se você realmente queria que as pessoas soubessem que o ponteiro "deu mal", vou lançar 100% de acordo com aqueles que dizem que a melhor coisa a fazer é fazer com que ele saia do escopo. Então você está usando o compilador para impedir a possibilidade de maus desreferências em tempo de execução.

Esse é o bebê em toda a água do banho C ++, não deve jogar fora. :)

Em um programa bem estruturado com verificação de erro apropriada, não há razão não para atribuí -lo nulo. 0 permanece sozinho como um valor inválido universalmente reconhecido nesse contexto. Falhar com força e falhar em breve.

Muitos dos argumentos contra a atribuição 0 sugira que isso poderia ocultar um bug ou complicar o fluxo de controle. Fundamentalmente, esse é um erro a montante (não é sua culpa (desculpe pelo trocadilho ruim) ou outro erro em nome do programador - talvez até uma indicação de que o fluxo do programa tenha crescido muito complexo.

Se o programador desejar introduzir o uso de um ponteiro que pode ser nulo como um valor especial e escrever toda a esquiva necessária em torno disso, essa é uma complicação que eles introduziram deliberadamente. Quanto melhor a quarentena, mais cedo você encontrará casos de uso indevido, e menos eles são capazes de se espalhar para outros programas.

Programas bem estruturados podem ser projetados usando recursos de C ++ para evitar esses casos. Você pode usar referências ou apenas dizer "passar/usar argumentos nulos ou inválidos é um erro" - uma abordagem igualmente aplicável a contêineres, como ponteiros inteligentes. Aumentar o comportamento consistente e correto proíbe que esses bugs se afastem.

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 a 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 os engenheiros não puderem seguir o programa após uma leitura rápida ou houver um fluxo inadequado de verificação de erros ou inconsistente/indiferente, você terá outros problemas maiores.

Por fim, seu compilador e ambiente provavelmente têm alguns guardas para os momentos em que você deseja introduzir erros (rabiscando), detecte acessos à memória liberada e capturar outro UB relacionado. Você também pode introduzir diagnósticos semelhantes em seus programas, geralmente sem afetar os programas existentes.

Deixe -me expandir o que você já fez em sua pergunta.

Aqui está o que você fez em sua pergunta, em forma de marcador:


Definir ponteiros para NULL após a exclusão não é uma boa prática universal em C ++. Há momentos em que:

  • É bom fazer
  • e momentos em que é inútil e pode ocultar erros.

No entanto, existe Sem momentos Quando isso é mau! Você irá não introduzir mais bugs anulando explicitamente, você não vai vazar Memória, você não vai causar comportamento indefinido acontecer.

Então, em caso de dúvida, apenas nulo.

Dito isto, se você sentir que precisa explicitamente anular um pouco de ponteiro, então para mim isso parece que você não dividiu um método o suficiente e deve olhar para a abordagem de refatoração chamada "Método Extrato" para dividir o método no peças separadas.

Sim.

O único "mal" que ele pode fazer é introduzir ineficiência (desnecessário, operação de loja) em seu programa, mas essa sobrecarga vai 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, você vai tem alguns desagradáveis ponteiro derefernce bugs de um dia.

Eu sempre utilizar uma macro para excluir:

#define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; }

(e semelhante para uma matriz de higiene(), liberando as alças)

Você também pode escrever "auto-exclusão" métodos que levam uma referência para o código de chamada do ponteiro, para que eles vigor o código de chamada do ponteiro para NULL.Por exemplo, para eliminar uma subárvore de muitos objetos:

static void TreeItem::DeleteSubtree(TreeItem *&rootObject)
{
    if (rootObject == NULL)
        return;

    rootObject->UnlinkFromParent();

    for (int i = 0; i < numChildren)
       DeleteSubtree(rootObject->child[i]);

    delete rootObject;
    rootObject = NULL;
}

editar

Sim, essas técnicas violar algumas regras sobre o uso de macros (e sim, esses dias você provavelmente poderia obter o mesmo resultado com modelos), mas usando ao longo de muitos anos eu nunca, jamais, acessado mortos memória - um dos piores e mais difícil e mais demorado para depurar problemas que você pode enfrentar.Na prática, ao longo de muitos anos, elas têm efetivamente eliminou um whjole classe de erros de cada equipe, eu ter introduzido a eles.

Há também muitas maneiras de implementar o acima - estou apenas tentando ilustrar a idéia de forçar as pessoas a um ponteiro NULO se excluir um objeto, em vez de fornecer um meio para eles para liberar a memória que não NULO do chamador ponteiro.

Claro, o exemplo acima é apenas um passo em direção a uma auto-ponteiro.Que eu não sugere porque o OP foi especificamente perguntando sobre o caso de não utilizar um auto ponteiro.

"Há momentos em que é uma coisa boa a se fazer, e momentos em que é inútil e pode ocultar erros"

Eu posso ver dois problemas: esse código simples:

delete myObj;
myobj = 0

Torna-se para uma liner em ambiente multithread:

lock(myObjMutex); 
delete myObj;
myobj = 0
unlock(myObjMutex);

A "prática recomendada" de Don Neufeld nem se aplica sempre. Por exemplo, em um projeto automotivo, tivemos que definir ponteiros para 0, mesmo em destruidores. Eu posso imaginar em software crítico de segurança que essas regras não são incomuns. É mais fácil (e sábio) segui-los do que tentar convencer a equipe/verificador de código para cada uso de ponteiro no código, que uma linha anulando esse ponteiro é redundante.

Outro perigo é confiar nessa técnica no código de uso de exceções:

try{  
   delete myObj; //exception in destructor
   myObj=0
}
catch
{
   //myObj=0; <- possibly resource-leak
}

if (myObj)
  // use myObj <--undefined behaviour

Nesse código, você produz-Leak de recursos e adie o problema ou o processo trava.

Então, esses dois problemas passam espontaneamente pela minha cabeça (a Herb Sutter com certeza diria mais) faz com que todas as perguntas do tipo "Como evitar usar ponteiros inteligentes e fazer o trabalho com segurança com ponteiros normais" sejam obsoletos.

Sempre há Ponteiros pendurados se preocupar com.

Se você for realocar o ponteiro antes de usá -lo novamente (desreferenciando -o, passando para uma função etc.), fazer com que o ponteiro nulo seja apenas uma operação extra. No entanto, se você não tem certeza se será realocado ou não antes de ser usado novamente, defini -lo como NULL é uma boa ideia.

Como muitos disseram, é claro que é muito mais fácil usar indicadores inteligentes.

EDIT: Como Thomas Matthews disse em esta resposta anterior, se um ponteiro for excluído em um destruidor, não há necessidade de atribuir nulo, pois não será usado novamente porque o objeto já está sendo destruído.

Eu posso imaginar definir um ponteiro para nulo depois de excluí -lo sendo útil em casos raros em que há um legítimo cenário de reutilizá -lo em uma única função (ou objeto). Caso contrário, não faz sentido - um ponteiro precisa apontar para algo significativo desde que exista - período.

Se o código não pertencer à parte mais crítica do seu aplicativo, mantenha-o simples e use um shared_ptr:

shared_ptr<Foo> p(new Foo);
//No more need to call delete

Ele executa a contagem de referência e é segura por threads. Você pode encontrá -lo no espaço de nome TR1 (std :: tr1, #include <memória>) ou se o seu compilador não o fornecer, obtenha -o do Boost.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top