C Ponteiro Nulo (NULL e nullptr)

Na aula Ponteiros você viu que um ponteiro sem valor inicial guarda lixo, um endereço qualquer, e que isso é perigoso. Mas às vezes você ainda não tem para onde apontar: a pessoa não escolheu uma opção, a busca não achou nada, o arquivo não abriu. Para isso existe o ponteiro nulo: um valor especial que quer dizer "não aponta para nada", e que dá para testar. Esta aula mostra como criá-lo, como conferir antes de usar e o que acontece quando alguém esquece de conferir.

Você vai aprender
  • O que é um ponteiro nulo e por que ele é melhor que lixo.
  • NULL (de onde vem, que #include pede) e o nullptr do C23.
  • Como conferir um ponteiro antes de usar: if (p != NULL) e a forma curta if (p).
  • O que acontece ao desreferenciar um ponteiro nulo: Falha de segmentação, código 139.
  • Por que, às vezes, o que o programa escreveu antes de morrer some (o buffer do printf).

Os programas foram compilados na máquina da Ana, na pasta ~/c, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa.


A ideia: o bilhete em branco

Lembra do bilhete com o número da gaveta? Existem três situações possíveis para ele:

O bilheteEm CDá para confiar?
Tem o número de uma gaveta de verdadeint *p = &x;Sim: *p é o x.
Tem um número rabiscado ao acasoint *p; (lixo)Não, e não há como saber olhando.
Está escrito, bem grande: "NENHUMA"int *p = NULL;Não aponta para nada, e você sabe disso: dá para perguntar antes de ir até a gaveta.

O ponteiro nulo é o terceiro bilhete. Ele não resolve tudo sozinho: ir até a gaveta "NENHUMA" continua sendo um erro. A vantagem é que esse erro pode ser evitado com um if, e, quando não é evitado, o programa morre na hora e no lugar certo, em vez de estragar dados em silêncio.

42&x0x??????NULLxp1: apontap2: lixo (?)p3: nulop1 → x
Só o p1 leva a algum lugar. O p2 pode levar a qualquer gaveta, e não há como descobrir. O p3 diz claramente "nenhuma": basta testar.

1. Criar e testar um ponteiro nulo

#include <stdio.h>

int main(void) {
    int *p = NULL;

    printf("p guarda %p\n", (void *) p);
    if (p == NULL) {
        printf("p não aponta para nada\n");
    }
    return 0;
}
$ gcc -std=c23 -Wall -Wextra nulo.c -o nulo
$ ./nulo
p guarda (nil)               « o jeito da glibc de mostrar o ponteiro nulo
p não aponta para nada
int *p = NULL;
Cria o ponteiro já "apontando para nada". NULL (lê-se "nul") é uma constante que vem nos cabeçalhos da biblioteca padrão. O stdio.h já a traz, por isso este programa não precisou de outro #include.
p == NULL
Compara o endereço guardado em p com o nulo. Repare: sem *. Você pergunta sobre o bilhete, sem ir até gaveta nenhuma.
(nil)
É assim que o %p da glibc (a biblioteca C do Linux) mostra um ponteiro nulo: "nil", nada. No editor do guia, que usa outra biblioteca, o mesmo programa mostra p guarda 0.

Por dentro, o nulo é o endereço 0

Na prática, NULL é o endereço zero, uma gaveta que o sistema nunca entrega a programa nenhum. Por isso escrever int *q = 0; também cria um ponteiro nulo, e um ponteiro nulo vale "falso" num if:

#include <stdio.h>

int main(void) {
    int *p = NULL;
    int *q = 0;
    int x = 5;
    int *r = &x;

    printf("p == q? %d\n", p == q);
    printf("p é falso? %d\n", !p);
    printf("r é falso? %d\n", !r);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra zero.c -o zero
$ ./zero
p == q? 1          « NULL e 0 dão o mesmo ponteiro
p é falso? 1       « nulo conta como falso
r é falso? 0       « um ponteiro que aponta para algo conta como verdadeiro

Daí vem a forma curta de testar, muito comum em código C:

Forma longaForma curtaPergunta
if (p != NULL)if (p)"p aponta para alguma coisa?"
if (p == NULL)if (!p)"p é nulo?"

As duas fazem exatamente o mesmo. A forma longa é mais clara para quem está começando; a curta é a que você mais vai encontrar em projetos.


2. nullptr: o nulo do C23 C23

O C23 criou uma palavra-chave própria para o ponteiro nulo, nullptr (de "null pointer", ponteiro nulo), que não precisa de #include nenhum. O gcc 15 e o Clang do editor aceitam:

#include <stdio.h>

int main(void) {
    int *p = nullptr;

    if (p == nullptr) {
        printf("p é nulo\n");
    }
    printf("p == NULL? %d\n", p == NULL);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra nullptr.c -o nullptr
$ ./nullptr
p é nulo
p == NULL? 1           « nullptr e NULL dão o mesmo ponteiro nulo

Por que criar outra forma, se NULL já existia? Porque NULL é uma macro, e cada biblioteca a define de um jeito: às vezes como ((void *)0), às vezes como um simples 0. Um 0 também é um número inteiro, e isso causa confusões em situações avançadas (como nas funções variádicas, as de número variável de argumentos, como o próprio printf). O nullptr é sempre e só um ponteiro nulo, com um tipo próprio, nullptr_t.

O preço: só existe a partir do C23. Com um padrão mais antigo, ele não é reconhecido:

$ gcc -std=c17 -Wall -Wextra nullptr.c -o nullptr
nullptr.c: In function ‘main’:
nullptr.c:4:14: error: ‘nullptr’ undeclared (first use in this function)
    4 |     int *p = nullptr;
      |              ^~~~~~~
nullptr.c:4:14: note: each undeclared identifier is reported only once for each function it appears in
FormaDesdePrecisa deUse?
NULLsempre (C89)stddef.h (ou stdio.h, stdlib.h, string.h…)Sim. Funciona em qualquer compilador e é o que 99% do código existente usa. É o que este guia usa na maioria dos exemplos.
nullptrC23nadaSim, em código novo, se todos que vão compilar têm gcc 13+ ou Clang 16+.
0semprenadaEvite: funciona, mas não deixa claro que é um ponteiro.

3. Ir até a gaveta "NENHUMA": desreferenciar o nulo

Testar p é seguro. Usar *p quando p é nulo, não: é ir até a gaveta zero, que não pertence ao programa.

#include <stdio.h>

int main(void) {
    int *p = NULL;

    printf("Antes de usar o ponteiro\n");
    printf("*p vale %d\n", *p);
    printf("Depois\n");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra desref.c -o desref
$ ./desref
Antes de usar o ponteiro
Falha de segmentação     (imagem do núcleo gravada) ./desref
$ echo $?
139

Compilou sem nenhum aviso, rodou a primeira linha e morreu na segunda. Vamos ler a mensagem, que é do terminal (o bash), não do seu programa:

Falha de segmentação
Em inglês, segmentation fault ("segfault"). O programa tentou mexer numa região da memória (um "segmento") que não é dele, e o sistema o matou na hora.
(imagem do núcleo gravada)
Em inglês, core dumped. O sistema pode salvar uma "fotografia" da memória do programa no momento da morte, para estudar depois com um depurador. Você não precisa fazer nada com ela agora; a aula Depurar com gdb mostra outro jeito de investigar.
./desref
Qual comando morreu.
139
O código de saída. Quando um programa é morto por um sinal, o código é 128 + o número do sinal. A falha de segmentação é o sinal 11 (SIGSEGV): 128 + 11 = 139. Veja Sinais.

No editor do guia, o resultado é o mesmo: a saída mostra Antes de usar o ponteiro, depois Falha de segmentação (imagem do núcleo gravada) e [código de saída: 139]. Experimente:

Experimente: rode e veja o programa morrer na linha do *p. Depois troque NULL por &x e rode de novo.

#include <stdio.h>

int main(void) {
    int x = 42;
    int *p = NULL;

    printf("x vale %d\n", x);
    printf("Antes de usar o ponteiro\n");
    printf("*p vale %d\n", *p);
    printf("Depois\n");
    return 0;
}

A saída que some: o buffer do printf

Agora rode o mesmo programa mandando a saída para um arquivo, em vez da tela:

$ ./desref > saida.txt
Falha de segmentação     (imagem do núcleo gravada) ./desref > saida.txt
$ cat saida.txt
$                            « o arquivo está vazio!

O Antes de usar o ponteiro sumiu. O motivo: o printf não escreve cada letra na hora. Ele junta o texto num buffer (um balde) e só despeja o balde de tempos em tempos, porque despejar de uma vez é muito mais rápido. A regra da glibc é:

  • Se a saída é a tela (um terminal), o balde é despejado a cada \n. Por isso, no terminal, a primeira linha apareceu.
  • Se a saída é um arquivo ou um pipe (|), o balde só é despejado quando enche (alguns milhares de letras) ou quando o programa termina normalmente.

Um programa morto por falha de segmentação não termina normalmente: o balde vai junto com ele, sem ser despejado. Isso engana muita gente, que procura o erro no lugar errado ("se nem o primeiro printf apareceu, o erro deve estar antes dele!"). Duas defesas:

  • fflush(stdout); logo depois de um printf força o despejo do balde.
  • Mensagens de depuração vão para stderr, que não tem balde: fprintf(stderr, "chegou aqui\n"); (veja stdin, stdout e stderr).
Pergunta sem \n Um printf("Quantos anos? "); sem \n fica no balde mesmo no terminal. Ele aparece na tela porque, quando o programa vai ler o teclado (scanf), a glibc despeja o balde antes. Se a entrada vier de um pipe (echo 7 | ./programa), esse despejo não acontece, e uma pergunta seguida de uma falha de segmentação some da tela. Você vai ver isso no "Quando dá errado".

Descobrir a linha do erro

A mensagem Falha de segmentação não diz onde o programa morreu. Num programa grande, a forma mais rápida de descobrir é compilar com o AddressSanitizer (-fsanitize=address) e -g, que acrescentam ao programa um "fiscal" da memória:

$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address desref.c -o desref-asan
$ ./desref-asan
Antes de usar o ponteiro
AddressSanitizer:DEADLYSIGNAL
=================================================================
==58052==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x59e405ebe267 bp 0x7ffe3dcbd470 sp 0x7ffe3dcbd460 T0)
==58052==The signal is caused by a READ memory access.
==58052==Hint: address points to the zero page.
    #0 0x59e405ebe267 in main /home/ana/c/desref.c:7
    #1 0x77d13c82a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
    #2 0x77d13c82a717 in __libc_start_main_impl ../csu/libc-start.c:360
    #3 0x59e405ebe144 in _start (/home/ana/c/desref-asan+0x1144) (BuildId: ad68b35b2bf9594f8e02e5ad0bf5bd7c00457bab)
…                            « (os valores dos registradores do processador, omitidos aqui)
SUMMARY: AddressSanitizer: SEGV /home/ana/c/desref.c:7 in main
==58052==ABORTING

As linhas importantes: SEGV on unknown address 0x000000000000 (falha de segmentação no endereço zero, o nulo), READ memory access (foi uma leitura), address points to the zero page (o endereço está na "página zero", a do nulo) e, principalmente, desref.c:7: linha 7, a do *p. O sanitizer não funciona no editor do guia; a aula Sanitizers mostra mais.


4. Sempre conferir antes de usar

O uso típico do ponteiro nulo: um ponteiro que talvez receba um endereço. Ele nasce nulo, alguma decisão do programa pode apontá-lo para algo, e, antes de usar, você confere. A Ana quer ver os pontos de uma pessoa do jogo:

#include <stdio.h>

int main(void) {
    int pontos_ana = 30;
    int pontos_bia = 45;
    int *escolhido = NULL;
    int opcao = 0;

    printf("Ver os pontos de quem? (1 = Ana, 2 = Bia): ");
    if (scanf("%d", &opcao) != 1) {
        printf("Digite um número.\n");
        return 1;
    }

    if (opcao == 1) {
        escolhido = &pontos_ana;
    } else if (opcao == 2) {
        escolhido = &pontos_bia;
    }

    if (escolhido != NULL) {
        printf("Pontos: %d\n", *escolhido);
    } else {
        printf("Opção inválida.\n");
    }
    return 0;
}
$ gcc -std=c23 -Wall -Wextra escolha.c -o escolha
$ ./escolha
Ver os pontos de quem? (1 = Ana, 2 = Bia): 2
Pontos: 45
$ ./escolha
Ver os pontos de quem? (1 = Ana, 2 = Bia): 7
Opção inválida.          « nenhum if apontou o ponteiro: ele continuou NULL
LinhaPor que está lá
int *escolhido = NULL;O ponteiro nasce dizendo "nenhum". Nunca com lixo.
escolhido = &pontos_bia;Só aponta se a opção for válida.
if (escolhido != NULL)Antes de ir até a gaveta, confere se existe gaveta.

Esse padrão vai aparecer o tempo todo, porque muitas funções da biblioteca padrão devolvem NULL quando falham: fopen quando o arquivo não abre (Arquivos), malloc quando falta memória (malloc e free), strchr quando não acha a letra (Funções de String), getenv quando a variável não existe (getenv), fgets quando a entrada acaba (fgets). Em todas, a regra é a mesma: confira o retorno antes de usar.

Conferir e usar na mesma condição

O && só olha o lado direito se o esquerdo for verdadeiro (veja Operadores Lógicos). Por isso esta linha é segura, mesmo quando p é nulo:

if (p != NULL && *p > 0) {     // se p for nulo, o *p nem é avaliado
    printf("positivo\n");
}

Inverter a ordem (*p > 0 && p != NULL) estraga tudo: o *p seria avaliado primeiro.

Missão: este programa esqueceu de conferir o ponteiro, e com a entrada 7 ele morre com Falha de segmentação. Rode para ver. Depois coloque um if que só mostra os pontos quando escolhido não for nulo e, senão, mostra Opção inválida.

Ver os pontos de quem? (1 = Ana, 2 = Bia): 7
Opção inválida.
#include <stdio.h>

int main(void) {
    int pontos_ana = 30;
    int pontos_bia = 45;
    int *escolhido = NULL;
    int opcao = 0;

    printf("Ver os pontos de quem? (1 = Ana, 2 = Bia): ");
    if (scanf("%d", &opcao) != 1) {
        printf("Digite um número.\n");
        return 1;
    }

    if (opcao == 1) {
        escolhido = &pontos_ana;
    } else if (opcao == 2) {
        escolhido = &pontos_bia;
    }

    printf("Pontos: %d\n", *escolhido);
    return 0;
}
#include <stdio.h>

int main(void) {
    int pontos_ana = 30;
    int pontos_bia = 45;
    int *escolhido = NULL;
    int opcao = 0;

    printf("Ver os pontos de quem? (1 = Ana, 2 = Bia): ");
    if (scanf("%d", &opcao) != 1) {
        printf("Digite um número.\n");
        return 1;
    }

    if (opcao == 1) {
        escolhido = &pontos_ana;
    } else if (opcao == 2) {
        escolhido = &pontos_bia;
    }

    if (escolhido != NULL) {
        printf("Pontos: %d\n", *escolhido);
    } else {
        printf("Opção inválida.\n");
    }
    return 0;
}

Hábitos que evitam o problema

  1. Todo ponteiro nasce com valor: o endereço de algo, ou NULL. Nunca int *p; sozinho.
  2. Confira antes de usar todo ponteiro que pode ser nulo, principalmente os que vêm de funções (fopen, malloc, strchr…).
  3. Depois de "jogar fora" o que o ponteiro apontava, volte-o para NULL. Isso fará sentido em malloc e free: um ponteiro para memória já devolvida parece válido, mas não é; um nulo, pelo menos, dá para testar.
  4. Compile com -Wall -Wextra e, quando um programa morrer sem explicação, rode-o com -g -fsanitize=address.

Quando dá errado

"‘NULL’ undeclared": faltou o #include
int main(void) {
    int *p = NULL;
    return p == 0;
}
$ gcc -std=c23 -Wall -Wextra semnull.c -o semnull
semnull.c: In function ‘main’:
semnull.c:2:14: error: ‘NULL’ undeclared (first use in this function)
    2 |     int *p = NULL;
      |              ^~~~
semnull.c:1:1: note: ‘NULL’ is defined in header ‘<stddef.h>’; this is probably fixable by adding ‘#include <stddef.h>’
  +++ |+#include <stddef.h>
    1 | int main(void) {
semnull.c:2:14: note: each undeclared identifier is reported only once for each function it appears in
    2 |     int *p = NULL;
      |              ^~~~

O que significa: "NULL não foi declarado". NULL não faz parte da linguagem: vem de um cabeçalho. O próprio gcc dá a dica: "NULL é definido em <stddef.h>; provavelmente se resolve acrescentando #include <stddef.h>". No editor, o Clang diz use of undeclared identifier 'NULL'.

Como consertar: inclua <stddef.h>, ou qualquer cabeçalho que você já usaria (stdio.h, stdlib.h e string.h também trazem o NULL). Ou, em C23, use nullptr, que não precisa de cabeçalho.

"‘nullptr’ undeclared": padrão antigo
$ gcc -std=c17 -Wall -Wextra nullptr.c -o nullptr
nullptr.c: In function ‘main’:
nullptr.c:4:14: error: ‘nullptr’ undeclared (first use in this function)
    4 |     int *p = nullptr;
      |              ^~~~~~~
nullptr.c:4:14: note: each undeclared identifier is reported only once for each function it appears in

O que significa: o nullptr só existe a partir do C23, e o comando pediu o C17.

Como consertar: compile com -std=c23 (o gcc 15 já usa o C23 se você não pedir outro padrão) ou use NULL.

"Falha de segmentação" e a pergunta sumiu
$ gcc -std=c23 -Wall -Wextra escolha-sem.c -o escolha-sem
$ echo 7 | ./escolha-sem
Falha de segmentação     (imagem do núcleo gravada) echo 7 | ./escolha-sem
$ echo $?
139

O que aconteceu: é o programa da missão, sem o if. Com a opção 7, escolhido continuou NULL, e o *escolhido matou o programa. A pergunta "Ver os pontos de quem?" nem apareceu: ela estava no balde do printf (sem \n, e com a entrada vindo de um pipe), e o balde morreu junto. Digitando o 7 no teclado, a pergunta apareceria antes.

Como consertar: confira o ponteiro antes de usar (if (escolhido != NULL)). Para achar a linha, compile com -g -fsanitize=address.

"O nulo aparece como (nil) no Linux e como 0 no editor"

O que aconteceu: nada de errado. O formato que o %p usa para o nulo não é definido pelo padrão do C: cada biblioteca escolhe o seu. A glibc do Linux escreve (nil); a biblioteca do editor escreve 0.

Como resolver: para saber se um ponteiro é nulo, teste com if (p == NULL); não dependa do texto que o %p mostra.


Referência rápida

CódigoO que faz
int *p = NULL;Cria um ponteiro que não aponta para nada (precisa de stddef.h, stdio.h ou outro cabeçalho padrão).
int *p = nullptr;O mesmo, em C23, sem cabeçalho.
if (p != NULL) / if (p)Verdadeiro se p aponta para algo.
if (p == NULL) / if (!p)Verdadeiro se p é nulo.
if (p && *p > 0)Confere e usa na mesma condição (o *p só roda se p não for nulo).
*p com p nuloFalha de segmentação: o programa morre com código 139.
printf("%p", (void *) NULL)(nil) no Linux; 0 no editor do guia.
fflush(stdout);Despeja o balde do printf agora.
gcc -g -fsanitize=address …Mostra a linha da falha de segmentação.

Exercícios

1. Nasce nulo

Complete para criar o ponteiro achado sem apontar para nada (use a forma que funciona em qualquer versão do C):

int *achado = ;

2. Conferir antes

Complete a condição para só usar *p quando p apontar para algo (forma longa):

if (p ) { printf("%d\n", *p); }

3. O código

Um programa morreu com falha de segmentação. Qual número o echo $? mostra logo depois?

$ echo $? →

4. Por que int *p = NULL; é melhor que int *p;?

5. Um programa morreu com falha de segmentação e o printf("Início\n") da primeira linha não apareceu no arquivo de saída. O mais provável é que…

6. Qual condição é segura mesmo quando p é nulo?

Missão final: achou ou não achou?

Missão: a Ana anotou os saldos de 5 clientes. Complete o programa: (1) no laço, quando achar o primeiro saldo negativo, faça achado apontar para ele (&saldos[i]) e pare com break; (2) depois do laço, confira o achado: se não for nulo, mostre Primeiro negativo: e o valor, e zere esse saldo pelo ponteiro; se for nulo, mostre Nenhum negativo.

Primeiro negativo: -15
Depois: 120 80 0 40 -3
#include <stdio.h>

int main(void) {
    int saldos[5] = {120, 80, -15, 40, -3};
    int *achado = NULL;

    for (int i = 0; i < 5; i++) {
        // se saldos[i] for negativo: aponte achado para ele e pare
    }

    // confira achado: mostre o valor e zere pelo ponteiro,
    // ou mostre "Nenhum negativo"

    printf("Depois:");
    for (int i = 0; i < 5; i++) {
        printf(" %d", saldos[i]);
    }
    printf("\n");
    return 0;
}
#include <stdio.h>

int main(void) {
    int saldos[5] = {120, 80, -15, 40, -3};
    int *achado = NULL;

    for (int i = 0; i < 5; i++) {
        if (saldos[i] < 0) {
            achado = &saldos[i];
            break;
        }
    }

    if (achado != NULL) {
        printf("Primeiro negativo: %d\n", *achado);
        *achado = 0;
    } else {
        printf("Nenhum negativo\n");
    }

    printf("Depois:");
    for (int i = 0; i < 5; i++) {
        printf(" %d", saldos[i]);
    }
    printf("\n");
    return 0;
}

Resumo

  • O ponteiro nulo não aponta para nada, e, ao contrário do lixo, dá para testar.
  • NULL vem dos cabeçalhos padrão (stddef.h, stdio.h, stdlib.h…) e funciona em qualquer C. nullptr é a palavra-chave do C23, sem cabeçalho.
  • Teste com if (p != NULL) ou if (p); nulo vale falso.
  • Desreferenciar o nulo (*p) mata o programa: Falha de segmentação (imagem do núcleo gravada), código 139, no Linux e no editor.
  • Quando o programa morre, o que estava no balde do printf some (principalmente com a saída num arquivo ou pipe). Use fflush(stdout) ou stderr para mensagens de depuração, e -g -fsanitize=address para achar a linha.
  • Funções como fopen, malloc, strchr e getenv devolvem NULL quando falham: confira sempre. Próxima aula: Ponteiros e Arrays.