O que significa fazer uma "verificação nula" em C ou C ++?

21

Eu tenho aprendido C ++ e estou tendo dificuldade em entender nulo. Em particular, os tutoriais que li mencionam fazer uma "verificação nula", mas não tenho certeza do que isso significa ou por que é necessário.

  • O que exatamente é nulo?
  • O que significa "procurar nulo"?
  • Sempre preciso verificar se há nulo?

Qualquer exemplo de código seria muito apreciado.

kdi
fonte
Quando: programmers.stackexchange.com/questions/186036/…
Ciro Santilli escreveu:
Eu aconselharia a obter alguns tutoriais melhores, se todos os que você ler falarem sobre verificações nulas sem nunca explicá-las e fornecer código de exemplo ... #
underscore_d

Respostas:

26

Em C e C ++, os ponteiros são inerentemente inseguros, ou seja, quando você desreferencia um ponteiro, é de sua responsabilidade garantir que ele aponte para algum lugar válido; isso faz parte do objetivo do "gerenciamento manual de memória" (em oposição aos esquemas de gerenciamento automático de memória implementados em linguagens como Java, PHP ou runtime do .NET, que não permitem criar referências inválidas sem grande esforço).

Uma solução comum que captura muitos erros é definir todos os ponteiros que não apontam para nada como NULL(ou, no C ++ correto 0) e verificar isso antes de acessar o ponteiro. Especificamente, é prática comum inicializar todos os ponteiros para NULL (a menos que você já tenha algo para apontá-los quando os declarar) e configurá-los para NULL quando você deleteou free()eles (a menos que eles fiquem fora do escopo imediatamente depois disso). Exemplo (em C, mas também em C ++ válido):

void fill_foo(int* foo) {
    *foo = 23; // this will crash and burn if foo is NULL
}

Uma versão melhor:

void fill_foo(int* foo) {
    if (!foo) { // this is the NULL check
        printf("This is wrong\n");
        return;
    }
    *foo = 23;
}

Sem a verificação nula, passar um ponteiro NULL para essa função causará um segfault, e não há nada que você possa fazer - o sistema operacional simplesmente interrompe seu processo e talvez despeje o núcleo ou apareça uma caixa de diálogo de relatório de falha. Com a verificação nula, você pode executar o tratamento adequado dos erros e se recuperar normalmente - corrija o problema sozinho, interrompa a operação atual, escreva uma entrada de log, notifique o usuário, o que for apropriado.

tdammers
fonte
3
@ MrLister, o que você quer dizer com verificações nulas não funcionam em C ++? Você só precisa inicializar o ponteiro para null quando o declara.
TZHX
1
O que quero dizer é que você deve se lembrar de definir o ponteiro como NULL ou não funcionará. E se você se lembrar, em outras palavras, se souber que o ponteiro é NULL, não precisará chamar fill_foo de qualquer maneira. fill_foo verifica se o ponteiro possui um valor, não se o ponteiro possui um valor válido . No C ++, não é garantido que os ponteiros sejam NULL ou tenham um valor válido.
Sr. Lister
4
Um assert () seria uma solução melhor aqui. Não faz sentido tentar "estar seguro". Se NULL foi repassado, está obviamente errado. Por que não travar explicitamente para tornar o programador totalmente ciente? (E, na produção, não importa, porque você já provou ? Que ninguém vai chamar fill_foo () com NULL, certo Realmente, não é tão difícil.)
Ambroz Bizjak
7
Não se esqueça de mencionar que uma versão ainda melhor dessa função deve usar referências em vez de ponteiros, tornando a verificação NULL obsoleta.
Doc Brown
4
Não é disso que se trata o gerenciamento manual de memória, e um programa gerenciado também explodirá (ou gerará uma exceção, pelo menos como um programa nativo na maioria dos idiomas), se você tentar desreferenciar uma referência nula.
Mason Wheeler
7

As outras respostas cobriram praticamente sua pergunta exata. Uma verificação nula é feita para garantir que o ponteiro que você recebeu realmente aponte para uma instância válida de um tipo (objetos, primitivas, etc.).

Vou adicionar meu próprio conselho aqui, no entanto. Evite verificações nulas. :) Verificações nulas (e outras formas de programação defensiva) desorganizam o código e, na verdade, o tornam mais propenso a erros do que outras técnicas de tratamento de erros.

Minha técnica favorita quando se trata de ponteiros de objeto é usar o padrão Null Object . Isso significa retornar uma (ponteiro - ou melhor ainda, referência a) matriz ou lista vazia em vez de nula, ou retornar uma string vazia ("") em vez de null, ou mesmo a string "0" (ou algo equivalente a "nothing "no contexto) em que você espera que seja analisado para um número inteiro.

Como bônus, aqui está uma coisinha que você talvez não soubesse sobre o ponteiro nulo, que foi (formalmente formalmente) implementado pelo CAR Hoare para a linguagem Algol W em 1965.

Eu chamo de erro do meu bilhão de dólares. Foi a invenção da referência nula em 1965. Naquela época, eu estava projetando o primeiro sistema abrangente de tipos para referências em uma linguagem orientada a objetos (ALGOL W). Meu objetivo era garantir que todo o uso de referências fosse absolutamente seguro, com a verificação realizada automaticamente pelo compilador. Mas não pude resistir à tentação de colocar uma referência nula, simplesmente porque era muito fácil de implementar. Isso levou a inúmeros erros, vulnerabilidades e falhas no sistema, que provavelmente causaram um bilhão de dólares de dor e danos nos últimos quarenta anos.

Yam Marcovic
fonte
6
Objeto nulo é ainda pior do que apenas ter um ponteiro nulo. Se um algoritmo X requer dados Y que você não possui, isso é um bug no seu programa , que você está simplesmente ocultando fingindo que possui .
DeadMG
Depende do contexto e, de qualquer forma, o teste de "presença de dados" é melhor do que o teste de nulo no meu livro. Pela minha experiência, se um algoritmo trabalha, digamos, em uma lista, e a lista está vazia, então o algoritmo simplesmente não tem nada a ver, e isso é realizado usando apenas instruções de controle padrão, como for / foreach.
Yam Marcovic
Se o algoritmo não tem nada a ver, por que você está chamando? E a razão pela qual você pode querer chamá-lo em primeiro lugar é porque ele faz algo importante .
DeadMG
@DeadMG Como os programas são sobre entrada e, no mundo real, diferentemente das tarefas de casa, a entrada pode ser irrelevante (por exemplo, vazia). O código ainda é chamado de qualquer maneira. Você tem duas opções: ou verifica a relevância (ou o vazio) ou projeta seus algoritmos para que eles leiam e funcionem bem sem verificar explicitamente a relevância usando instruções condicionais.
Yam Marcovic
Eu vim aqui para fazer quase o mesmo comentário, então dei o meu voto. No entanto, eu também acrescentaria que isso representa um problema maior de objetos zumbis - sempre que você tiver objetos com inicialização (ou destruição) em vários estágios que não estejam totalmente vivos, mas não completamente mortos. Quando você vê código "seguro" em idiomas sem finalização determinística que adicionou verificações em todas as funções para verificar se o objeto foi descartado, é esse problema geral que eleva sua cabeça. Você nunca deve se nulo, deve trabalhar com estados que possuam os objetos necessários para a vida útil.
Ex0du5
4

O valor do ponteiro nulo representa um "lugar nenhum" bem definido; é um valor de ponteiro inválido que é garantido para comparar com qualquer outro valor de ponteiro. Tentar desreferenciar um ponteiro nulo resulta em um comportamento indefinido e geralmente levará a um erro de tempo de execução. Portanto, você deseja garantir que um ponteiro não seja NULL antes de tentar desreferê-lo. Um número de funções da biblioteca C e C ++ retornará um ponteiro nulo para indicar uma condição de erro. Por exemplo, a função de biblioteca mallocretornará um valor de ponteiro nulo se não puder alocar o número de bytes que foram solicitados, e tentar acessar a memória através desse ponteiro (normalmente) levará a um erro de tempo de execução:

int *p = malloc(sizeof *p * N);
p[0] = ...; // this will (usually) blow up if malloc returned NULL

Portanto, precisamos garantir que a mallocchamada tenha êxito, verificando o valor de pcontra NULL:

int *p = malloc(sizeof *p * N);
if (p != NULL) // or just if (p)
  p[0] = ...;

Agora, segure suas meias por um minuto, isso vai ficar um pouco acidentado.

Há um valor de ponteiro nulo e uma constante de ponteiro nulo , e os dois não são necessariamente os mesmos. O valor do ponteiro nulo é o valor que a arquitetura subjacente usa para representar "lugar nenhum". Este valor pode ser 0x00000000 ou 0xFFFFFFFF ou 0xDEADBEEF ou algo completamente diferente. Não suponha que o valor do ponteiro nulo seja sempre 0.

A constante de ponteiro nulo , OTOH, é sempre uma expressão integral com valor 0. No que diz respeito ao seu código-fonte , 0 (ou qualquer expressão integral avaliada como 0) representa um ponteiro nulo. C e C ++ definem a macro NULL como a constante de ponteiro nulo. Quando seu código é compilado, a constante de ponteiro nulo será substituída pelo valor apropriado de ponteiro nulo no código de máquina gerado.

Além disso, esteja ciente de que NULL é apenas um dos muitos possíveis valores inválidos de ponteiro; se você declarar uma variável de ponteiro automático sem inicializá-la explicitamente, como

int *p;

o valor inicialmente armazenado na variável é indeterminado e pode não corresponder a um endereço de memória válido ou acessível. Infelizmente, não há uma maneira (portátil) de saber se um valor de ponteiro não NULL é válido ou não antes de tentar usá-lo. Portanto, se você estiver lidando com ponteiros, geralmente é uma boa ideia inicializá-los explicitamente para NULL quando você os declarar e defini-los como NULL quando não estiverem apontando ativamente para nada.

Observe que isso é mais um problema em C do que em C ++; o C ++ idiomático não deve usar tanto os ponteiros.

John Bode
fonte
3

Existem alguns métodos, todos essencialmente fazem a mesma coisa.

int * foo = NULL; // às vezes definido como 0x00 ou 0 ou 0L em vez de NULL

verificação nula (verifique se o ponteiro é nulo), versão A

if (foo == NULL)

verificação nula, versão B

if (! foo) // como NULL é definido como 0,! foo retornará um valor de um ponteiro nulo

verificação nula, versão C

if (foo == 0)

Dos três, prefiro usar a primeira verificação, pois ela explicitamente informa aos futuros desenvolvedores o que você estava tentando verificar E deixa claro que você esperava que foo fosse um ponteiro.


fonte
2

Você não O único motivo para usar um ponteiro em C ++ é porque você deseja explicitamente a presença de ponteiros nulos; caso contrário, você pode obter uma referência, que é semanticamente mais fácil de usar e garante que não é nulo.

DeadMG
fonte
1
@ James: 'novo' no modo kernel?
Nemanja Trifunovic
1
@ James: Uma implementação do C ++ que representa os recursos que uma maioria significativa dos codificadores C ++ desfruta. Isso inclui todos os recursos da linguagem C ++ 03 (exceto export) e todos os recursos da biblioteca C ++ 03 e TR1 e uma boa parte do C ++ 11.
DeadMG
5
I fazer gostaria que as pessoas não diria que "referências garantir não nulo." Eles não. É tão fácil gerar uma referência nula quanto um ponteiro nulo, e eles se propagam da mesma maneira.
Mjfgates
2
@ Stargazer: A pergunta é 100% redundante quando você apenas usa as ferramentas da maneira que os designers de linguagem e as boas práticas sugerem que você deveria.
DeadMG
2
@DeadMG, não importa se é redundante. Você não respondeu à pergunta . Eu direi novamente: -1.
riwalk
-1

Se você não verificar o valor NULL, especialmente se esse é um ponteiro para uma estrutura, talvez você encontre uma vulnerabilidade de segurança - desreferência de ponteiro NULL. A dereferência de ponteiro NULL pode levar a outras vulnerabilidades graves de segurança, como estouro de buffer, condição de corrida ... que podem permitir que o invasor assuma o controle do seu computador.

Muitos fornecedores de software como Microsoft, Oracle, Adobe, Apple ... lançam patches de software para corrigir essas vulnerabilidades de segurança. Eu acho que você deve verificar o valor NULL de cada ponteiro :)

oDisPo
fonte