Um ponteiro sempre guarda algum valor. Quando ele ainda não tem um objeto para apontar - antes de receber uma atribuição, depois que aquilo para onde ele apontava foi liberado, ou quando uma função não tem nada a devolver - ele precisa de um valor que signifique inequivocamente "não tem nada aqui". Esse valor é o ponteiro nulo, e NULL é o nome que você escreve.
A disciplina em torno dele é pequena e se paga o tempo todo: coloque NULL nos ponteiros que não têm alvo, verifique NULL antes de desreferenciar, e devolva NULL quando não houver resultado.
O que NULL realmente é
NULL é uma macro, definida em <stddef.h> e também trazida por <stdio.h>, <stdlib.h>, <string.h> e outros. Ela expande para uma constante de ponteiro nulo - tipicamente ((void*)0) ou simplesmente 0.
A norma do C garante a propriedade que importa: um ponteiro nulo compara como diferente de um ponteiro para qualquer objeto ou função real. Dois ponteiros nulos do mesmo tipo sempre comparam como iguais entre si.
A maioria dos sistemas imprime (nil) ou 0x0 para o ponteiro nulo - o endereço zero. Isso é uma implementação comum, não uma regra; a norma nunca promete que os bits são zero. O que ela promete é a comparação, então escreva p == NULL e nunca algo que dependa da representação.
Desreferenciar NULL trava o programa
A razão de existir de toda verificação de nulo do mundo:
int *p = NULL;
printf("%d\n", *p); // comportamento indefinido - quase certamente um crash
No Windows, no macOS e no Linux a primeira página do espaço de endereçamento é deixada sem mapeamento de propósito, então a CPU intercepta o acesso e o sistema operacional mata o programa. Você vai ver:
Segmentation fault (core dumped) # Linux
zsh: segmentation fault ./program # macOS
Esse travamento imediato é uma funcionalidade. Ele converte um bug de lógica numa falha barulhenta exatamente na linha que a causou, em vez de uma corrupção silenciosa que você descobre três semanas depois. Veja falha de segmentação para saber como ler o crash e achar a linha.
É por isso que "ponteiro nulo" não é uma exceção que você captura em C. Não existe mecanismo de recuperação; existe apenas verificar antes.
Verifique o que pode falhar
Funções que podem falhar sinalizam isso devolvendo NULL. Verificar não é uma gentileza opcional - a alternativa é o crash.
A biblioteca padrão é cheia dessas: malloc, calloc, realloc, fopen, strchr, strstr, getenv, bsearch. Todas elas respondem "sem resultado" com NULL. Veja memória dinâmica para a história completa da alocação.
Dois idiomas para a verificação, ambos comuns:
if (p == NULL) { /* trata */ } // explicito - preferido pela clareza
if (!p) { /* trata */ } // enxuto - depende de NULL ser falso
Um ponteiro nulo é avaliado como falso numa condição e um ponteiro válido como verdadeiro, então os dois funcionam. A forma explícita se lê melhor quando um desconhecido está varrendo seu código atrás de bugs.
NULL vs 0 vs não inicializado
Três situações distintas que iniciantes misturam:
int *a = NULL; "aponta para nada" - testavel, seguro de verificar
int *b; nao inicializado - guarda lixo, NAO e testavel
int *c = &x; aponta para um objeto real
O perigoso é o b. Seus bytes são o que tiver sobrado naquele espaço da pilha, que pode ser zero (e parecer tudo bem) ou pode ser um endereço plausível de uma chamada anterior. Nenhuma verificação consegue distingui-lo de um ponteiro válido, e o comportamento muda entre builds de depuração e de release.
Inicialize todos os ponteiros. Se você ainda não tem um endereço, NULL é o endereço. Compile com -Wall -Wextra e o compilador vai sinalizar muitos usos não inicializados para você.
Sobre o 0: num contexto de ponteiro a constante inteira 0 é a constante de ponteiro nulo, então p = 0; é C válido. Prefira NULL mesmo assim. Ele sinaliza a intenção, e faz diferença em chamadas variádicas, em que o compilador não pode converter por você:
execl("/bin/ls", "ls", 0); // arriscado - pode passar um int onde se espera um ponteiro
execl("/bin/ls", "ls", (char *)NULL); // correto
O C23 acrescenta nullptr, uma palavra-chave com tipo próprio que evita essa classe de ambiguidade por completo; NULL continua correto e portável em todo lugar.
Padrões defensivos
Proteja o topo de uma função que recebe ponteiros.
Coloque NULL depois de liberar. O free não altera o seu ponteiro - ele apenas devolve a memória. O ponteiro obsoleto que fica para trás é um ponteiro pendurado (dangling), e usá-lo é comportamento indefinido que muitas vezes não trava de imediato.
Dois fatos dentro desse exemplo merecem ser decorados. free(NULL) é definido como não fazer nada, então o código de limpeza nunca precisa proteger essa chamada. E colocar p = NULL depois do free transforma um uso-após-liberação - que pode corromper dados em silêncio - num crash imediato e depurável.
Devolva NULL para dizer "sem resultado", e diga isso.
// devolve um ponteiro para o elemento correspondente, ou NULL se nao houver nenhum
int *find(int *arr, int n, int target);
Documente isso no comentário acima da função. Quem sabe que um NULL é possível escreve a verificação; quem não sabe, não escreve.
Uma lista de conferência rápida
- Inicialize todos os ponteiros, com um endereço real ou com
NULL. - Verifique o retorno de
malloc,calloc,reallocefopenantes de usá-lo. - Verifique os parâmetros do tipo ponteiro no topo de qualquer função que possa ser chamada com entrada ruim.
- Coloque
NULLnos ponteiros imediatamente depois dofree. - Escreva
NULL, não0, sempre que a intenção for um ponteiro. - Compile com
-Wall -Wextrae, quando puder, execute sob um sanitizador:gcc -fsanitize=address,undefinedpega desreferências de nulo com um relatório preciso.
Perguntas frequentes
O que é um ponteiro NULL em C?
É um ponteiro que guarda a constante de ponteiro nulo - um valor que a norma garante ser diferente do endereço de qualquer objeto real. É a forma padrão de dizer "este ponteiro ainda não aponta para nada", e NULL é a macro que representa esse valor, definida em <stddef.h> e em vários outros cabeçalhos.
O que acontece se você desreferenciar um ponteiro NULL em C?
É comportamento indefinido. Na prática, em sistemas desktop e servidores, o programa trava na hora com uma falha de segmentação, porque o endereço zero é deixado sem mapeamento de propósito. Em sistemas embarcados sem proteção de memória ele pode silenciosamente ler ou corromper alguma coisa, o que é bem pior.
NULL é a mesma coisa que 0 em C?
Em contextos de ponteiro, sim: a constante inteira 0 é a constante de ponteiro nulo, então p = 0; e p == NULL funcionam. Mas use NULL para ponteiros e 0 para números - isso diz ao leitor qual dos dois você quis dizer, e mantém seguras as chamadas variádicas, em que um 0 cru pode ser passado como int em vez de ponteiro.
Qual a diferença entre um ponteiro NULL e um ponteiro não inicializado?
Um ponteiro NULL definitivamente não aponta para nada, e você pode testá-lo. Um ponteiro não inicializado guarda os bytes que por acaso estavam naquela memória - possivelmente um endereço com cara de válido - então não existe teste capaz de pegá-lo. Sempre inicialize ponteiros com NULL quando não tiver nada melhor.