Existe algum motivo para verificar um ponteiro NULL antes de excluir?

Muitas vezes, vejo código herdado verificando NULL antes de excluir um ponteiro, semelhante a

 if (NULL != pSomeObject) { delete pSomeObject; pSomeObject = NULL; } 

Existe algum motivo para verificar um ponteiro NULL antes de excluí-lo? Qual é o motivo para definir o ponteiro para NULL depois?

É perfeitamente “seguro” excluir um ponteiro nulo; Isso efetivamente equivale a um não-op.

O motivo pelo qual você pode querer verificar nulo antes de excluir é que tentar excluir um ponteiro nulo pode indicar um erro em seu programa.

O padrão C ++ garante que é legal usar um ponteiro nulo em uma expressão de exclusão (§8.5.2.5 / 2). No entanto, não é especificado se isso irá chamar uma function de desalocação ( operator delete ou operator delete[] ; §8.5.2.5 / 7, nota).

Se uma function de desalocação padrão (ou seja, fornecida pela biblioteca padrão) for chamada com um ponteiro nulo, a chamada não terá efeito (§6.6.4.4.2 / 3).

Mas não é especificado o que acontece se a function de desalocação não for fornecida pela biblioteca padrão – isto é, o que acontece quando sobrecarregamos a operator delete (ou o operator delete[] ).

Um programador competente manipularia pointers nulos adequadamente dentro da function de desalocação, em vez de antes da chamada, conforme mostrado no código do OP. Da mesma forma, definir o ponteiro como nullptr / NULL após a exclusão só serve a propósitos muito limitados. Algumas pessoas gostam de fazer isso no espírito da programação defensiva : isso tornará o comportamento do programa um pouco mais previsível no caso de um bug: acessar o ponteiro após a exclusão resultará em um access de ponteiro nulo ao invés de um access a um local de memory aleatória. Embora ambas as operações sejam um comportamento indefinido, o comportamento de um access de ponteiro nulo é muito mais previsível na prática (na maioria das vezes resulta em uma falha direta em vez de corrupção de memory). Como as corrupções de memory são especialmente difíceis de depurar, a redefinição de pointers excluídos ajuda na debugging.

– Claro que isso é tratar o sintoma ao invés da causa (ou seja, o bug). Você deve tratar os pointers de redefinição como cheiro de código. O código C ++ limpo e moderno tornará a propriedade da memory clara e estaticamente verificada (usando pointers inteligentes ou mecanismos equivalentes) e, assim, provavelmente evitará essa situação.

Bônus: Uma explicação da operator delete sobrecarregado:

operator delete é (apesar de seu nome) uma function que pode estar sobrecarregada como qualquer outra function. Essa function é chamada internamente para cada chamada do operator delete com argumentos correspondentes. O mesmo é verdadeiro para o operator new .

Sobrecarregar o operator new (e depois também operator delete ) faz sentido em algumas situações quando você deseja controlar precisamente como a memory é alocada. Fazer isso não é muito difícil, mas algumas precauções devem ser tomadas para garantir o comportamento correto. Scott Meyers descreve isso com grande detalhe C ++ efetivo .

Por enquanto, digamos que queremos sobrecarregar a versão global do operator new para debugging. Antes de fazermos isso, um breve aviso sobre o que acontece no código a seguir:

 klass* pobj = new klass; // … use pobj. delete pobj; 

O que realmente acontece aqui? Bem, o acima pode ser traduzido aproximadamente para o seguinte código:

 // 1st step: allocate memory klass* pobj = static_cast(operator new(sizeof(klass))); // 2nd step: construct object in that memory, using placement new: new (pobj) klass(); // … use pobj. // 3rd step: call destructor on pobj: pobj->~klass(); // 4th step: free memory operator delete(pobj); 

Observe a etapa 2, onde chamamos de new com uma syntax ligeiramente estranha. Esta é uma chamada para o chamado posicionamento new que recebe um endereço e constrói um object nesse endereço. Este operador também pode ser sobrecarregado. Neste caso, serve apenas para chamar o construtor da class klass .

Agora, sem mais demoras, aqui está o código para uma versão sobrecarregada dos operadores:

 void* operator new(size_t size) { // See Effective C++, Item 8 for an explanation. if (size == 0) size = 1; cerr << "Allocating " << size << " bytes of memory:"; while (true) { void* ret = custom_malloc(size); if (ret != 0) { cerr << " @ " << ret << endl; return ret; } // Retrieve and call new handler, if available. new_handler handler = set_new_handler(0); set_new_handler(handler); if (handler == 0) throw bad_alloc(); else (*handler)(); } } void operator delete(void* p) { cerr << "Freeing pointer @ " << p << "." << endl; custom_free(p); } 

Este código usa apenas uma implementação customizada de malloc / free internamente, assim como a maioria das implementações. Também cria uma saída de debugging. Considere o seguinte código:

 int main() { int* pi = new int(42); cout << *pi << endl; delete pi; } 

Ele produziu a seguinte saída:

 Allocating 4 bytes of memory: @ 0x100160 42 Freeing pointer @ 0x100160. 

Agora, esse código faz algo fundamentalmente diferente da implementação padrão de operator delete do operator delete : ele não testou pointers nulos! O compilador não verifica isso para que o código acima seja compilado, mas pode gerar erros desagradáveis ​​em tempo de execução quando você tenta excluir pointers nulos.

No entanto, como eu disse antes, esse comportamento é realmente inesperado e um escritor de biblioteca deve tomar cuidado para verificar se há pointers nulos na operator delete do operator delete . Esta versão é muito melhorada:

 void operator delete(void* p) { if (p == 0) return; cerr << "Freeing pointer @ " << p << "." << endl; free(p); } 

Em conclusão, embora uma implementação desleixada de operator delete do operator delete possa exigir verificações nulas explícitas no código do cliente, isso é um comportamento não padrão e só deve ser tolerado no suporte legado ( se houver ).

Excluir verificações para NULL internamente. Seu teste é redundante

A exclusão de null é um não operacional. Não há razão para procurar null antes de chamar delete.

Você pode querer verificar null por outros motivos, se o ponteiro que está sendo nulo tiver alguma informação adicional que lhe interessa.

De acordo com o C ++ 03 5.3.5 / 2, é seguro excluir um ponteiro nulo. Este seguinte é citado do padrão:

Em qualquer alternativa, se o valor do operando de exclusão for o ponteiro nulo, a operação não terá efeito.

Se pSomeObject for NULL, delete não fará nada. Portanto, não, você não precisa verificar NULL.

Consideramos uma boa prática atribuir NULL ao ponteiro após excluí-lo, se for possível que algum knucklehead tente usar o ponteiro. Usando um ponteiro NULL é um pouco melhor do que usando um ponteiro para quem sabe o que (o ponteiro NULL causará uma falha, o ponteiro para a memory excluída não pode)

Não há motivo para verificar NULL antes de excluir. Atribuir NULL após a exclusão pode ser necessário se em algum lugar nas verificações de código forem feitas se algum object já está alocado executando uma verificação NULL. Um exemplo seria algum tipo de dado em cache que é alocado sob demanda. Sempre que você limpa o object de cache, você atribui NULL ao ponteiro para que o código que aloca o object saiba que ele precisa executar uma alocação.

Depende do que você está fazendo. Algumas implementações mais antigas do free , por exemplo, não serão felizes se forem passadas com um ponteiro NULL . Algumas bibliotecas ainda apresentam esse problema. Por exemplo, o XFree na biblioteca Xlib diz:

DESCRIÇÃO

A function XFree é uma rotina Xlib de propósito geral que libera os dados especificados. Você deve usá-lo para liberar quaisquer objects que foram alocados pelo Xlib, a menos que uma function alternativa seja explicitamente especificada para o object. Um ponteiro NULL não pode ser passado para esta function.

Portanto, considere liberar os pointers NULL como um bug e você estará seguro.

Acredito que o desenvolvedor anterior tenha codificado “redundantemente” para economizar alguns milissegundos: é bom ter o ponteiro definido como NULL ao ser excluído, para que você possa usar uma linha como a seguinte logo após excluir o object:

 if(pSomeObject1!=NULL) pSomeObject1=NULL; 

Mas, em seguida, delete está fazendo essa comparação exata de qualquer maneira (não fazendo nada se for NULL). Por que isso duas vezes? Você sempre pode atribuir pSomeObject a NULL depois de chamar delete, independentemente de seu valor atual – mas isso seria um pouco redundante se já tivesse esse valor.

Então, minha aposta é que o autor dessas linhas tentou garantir que pSomeObject1 sempre seria NULL depois de ser excluído, sem incorrer no custo de um teste e atribuição potencialmente desnecessários.

Quanto às minhas observações, a exclusão de um ponteiro nulo usando delete é segura em máquinas baseadas em Unix ike PARISC e itanium. Mas não é seguro para os sistemas Linux, pois o processo falharia.