C++ nullptr

Na aula Ponteiros, você viu que um ponteiro sem valor guarda lixo, e que isso é perigoso. A solução é dar a ele um valor que significa claramente "não aponta para nada": o nullptr ("null pointer", ponteiro nulo). Nesta aula você vai ver como criar e testar um ponteiro nulo, o que acontece quando alguém esquece de testar (o programa cai, e você vai ver a mensagem real), e por que o C++ moderno trocou o antigo NULL pelo nullptr.

Você vai aprender
  • O que é um ponteiro nulo e como criar um: int* p = nullptr;.
  • Como perguntar "este ponteiro aponta para algo?": p != nullptr, if (p).
  • O que acontece ao desreferenciar um ponteiro nulo: Falha de segmentação, código 139.
  • Como o AddressSanitizer mostra a linha exata do erro.
  • A diferença entre nullptr, NULL e 0, com o erro real de ambiguidade.
  • O hábito que evita tudo isso: sempre checar antes de usar.

Os programas foram compilados na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: o papel em branco

Lembra do ponteiro como um papel com um endereço anotado? Existem três estados possíveis para esse papel:

O papelEm C++Dá para ir até a casa?
Com um endereço válido anotadoint* p = &x;Sim: *p é o x.
Escrito "nenhum endereço", bem legívelint* p = nullptr;Não, e você consegue saber que não dá.
Rabiscado, ilegívelint* p; (lixo)Não, e você não tem como saber.

O nullptr é o papel com "nenhum endereço" escrito em letra de forma. Ele não evita que alguém tente ir até a casa, mas permite conferir antes. É a diferença entre um formulário com "Telefone: não tem" e um formulário com o campo borrado.


1. Criar e testar um ponteiro nulo

#include <iostream>

int main() {
    int* p = nullptr;          // não aponta para nada (ainda)

    std::cout << "p = " << p << "\n";

    if (p == nullptr) {
        std::cout << "p não aponta para nada\n";
    }

    int pontos = 10;
    p = &pontos;

    if (p != nullptr) {
        std::cout << "Agora p aponta para um valor: " << *p << "\n";
    }
}
$ g++ -std=c++23 -Wall -Wextra nulo.cpp -o nulo
$ ./nulo
p = 0                        « o ponteiro nulo aparece como 0
p não aponta para nada
Agora p aponta para um valor: 10
nullptr
Uma palavra reservada do C++ C++11 que significa "ponteiro que não aponta para nada". Serve para ponteiros de qualquer tipo: int*, double*, Produto*.
p = 0
Na prática, o ponteiro nulo guarda o endereço 0. Esse endereço é reservado: nenhuma variável mora nele, e o sistema proíbe qualquer acesso ali. Por isso "0" funciona como "nenhum".
p == nullptr
"p é nulo?" Compara o endereço guardado com o nulo. Não usa *: você está olhando o papel, não indo até a casa.
p != nullptr
"p aponta para alguma coisa?" Só dentro desse if é seguro usar *p.

O jeito curto: if (p)

Um ponteiro pode ser usado direto como condição: nulo vale false, qualquer outro endereço vale true. Você vai ver muito este estilo:

#include <iostream>

int main() {
    int pontos = 10;
    int* p = nullptr;
    int* q = &pontos;

    if (!p) std::cout << "p é nulo\n";
    if (q)  std::cout << "q aponta para algo\n";
}
$ g++ -std=c++23 -Wall -Wextra curto.cpp -o curto
$ ./curto
p é nulo
q aponta para algo
Forma longaForma curtaPergunta
if (p != nullptr)if (p)"Aponta para algo?"
if (p == nullptr)if (!p)"É nulo?"

As duas formas fazem exatamente a mesma coisa. A longa é mais clara para quem está começando; use a que achar mais fácil de ler, mas seja consistente no mesmo programa. O ! é o "não" da aula Operadores Lógicos.


2. Desreferenciar um ponteiro nulo

O nullptr só protege quem confere. Se o programa seguir um ponteiro nulo com *p, ele vai até o endereço 0, que é proibido, e o sistema o mata na hora:

#include <iostream>

int main() {
    int* p = nullptr;
    std::cout << "Antes\n";
    std::cout << *p << "\n";      // segue um ponteiro nulo
    std::cout << "Depois\n";
}
$ g++ -std=c++23 -Wall -Wextra crash.cpp -o crash
$ ./crash
Antes
Falha de segmentação     (imagem do núcleo gravada) ./crash
$ echo $?
139
(compilou sem avisos)
O g++ não reclamou de nada, mesmo com -Wall -Wextra. Na maioria dos programas reais, o compilador não tem como saber se um ponteiro vai estar nulo; quem descobre é o programa, ao rodar.
Antes
A primeira linha rodou normalmente.
Falha de segmentação
O sistema matou o programa por acessar uma memória proibida. Num Linux em inglês, a mensagem é Segmentation fault (core dumped). "Segmentação" vem dos "segmentos" (pedaços) de memória que cada programa pode usar.
(imagem do núcleo gravada)
"Core dumped": o sistema pode ter guardado uma fotografia da memória do programa no momento da queda, para ser examinada com o GDB.
./crash
O comando que caiu.
139
O código de saída: 128 + 11. O 11 é o número do sinal SIGSEGV ("violação de segmento"). Quando um programa no terminal termina com 139, pense logo em ponteiro.

Repare: o Depois nunca apareceu. O programa morreu no meio, sem chance de salvar nada nem de mostrar uma mensagem amigável.

Descobrir a linha com o AddressSanitizer

Num programa grande, "Falha de segmentação" não diz onde. Compilando com -g -fsanitize=address (explicado em Sanitizers), o programa ganha um vigia que, na queda, mostra o arquivo e a linha:

$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address crash.cpp -o crash
$ ./crash
Antes
AddressSanitizer:DEADLYSIGNAL
=================================================================
==543431==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x562ba99f3271 bp 0x7ffcdef99220 sp 0x7ffcdef99210 T0)
==543431==The signal is caused by a READ memory access.
==543431==Hint: address points to the zero page.
    #0 0x562ba99f3271 in main /home/ana/cpp/crash.cpp:6
…                                « outras linhas da pilha e os registradores
SUMMARY: AddressSanitizer: SEGV /home/ana/cpp/crash.cpp:6 in main
==543431==ABORTING
  • SEGV on unknown address 0x000000000000: falha de segmentação no endereço 0, o nulo.
  • caused by a READ memory access: o programa tentou ler (era o std::cout << *p).
  • Hint: address points to the zero page: "dica: o endereço fica na página zero", o jeito do sanitizer dizer "isso é um ponteiro nulo".
  • #0 … in main /home/ana/cpp/crash.cpp:6: a função e a linha exata: a 6.
No editor do guia

O editor roda o programa em WebAssembly, e o guia faz o editor imitar o Linux nesse ponto: desreferenciar um ponteiro nulo também termina com Falha de segmentação e código 139. O que o editor não tem é o sanitizer, então o relatório detalhado aparece só no terminal da Ana. E lembre: em outros erros de memória (índice fora do array, memória já liberada) o editor fica calado — é justamente o perigo do comportamento indefinido, que pode passar despercebido num lugar e derrubar o programa em outro. As missões abaixo treinam o hábito que resolve em qualquer lugar: checar antes.

Missão: a função mostrar_preco recebe um ponteiro que pode ser nulo (produto sem preço cadastrado). Do jeito que está, ela segue o ponteiro sem conferir. Corrija: se preco for nulo, mostre Sem preço; senão, mostre Preço: e o valor.

#include <iostream>

void mostrar_preco(const double* preco) {
    // se preco for nulo, mostre "Sem preço"
    // senão, mostre "Preço: " e o valor apontado
    std::cout << "Preço: " << *preco << "\n";
}

int main() {
    double caneta = 4.5;
    mostrar_preco(&caneta);
    mostrar_preco(nullptr);
}
#include <iostream>

void mostrar_preco(const double* preco) {
    if (preco == nullptr) {
        std::cout << "Sem preço\n";
    } else {
        std::cout << "Preço: " << *preco << "\n";
    }
}

int main() {
    double caneta = 4.5;
    mostrar_preco(&caneta);
    mostrar_preco(nullptr);
}

3. nullptr × NULL × 0

Em código antigo, você vai encontrar outras duas formas de escrever "ponteiro nulo":

FormaDe onde vemO que é, de verdadeUse?
nullptrC++11 (2011)Um valor especial, do tipo std::nullptr_t, que só combina com ponteiros.Sim, sempre.
NULLHerdado do C (cabeçalho <cstddef>)Um apelido para o número 0, criado pelo pré-processador (no g++, um 0 especial chamado __null).Não em código novo.
0C e C++ antigosO número zero, que o C++ aceita converter em ponteiro nulo.Não.

Se as três "funcionam", por que o C++11 criou uma quarta coisa? Porque NULL e 0 são, no fundo, números, e isso confunde o compilador quando existe uma função para números e outra para ponteiros (funções com o mesmo nome e parâmetros diferentes são a sobrecarga):

#include <iostream>
#include <cstddef>

void mostrar(int)  { std::cout << "chamou mostrar(int)\n"; }
void mostrar(int*) { std::cout << "chamou mostrar(int*)\n"; }

int main() {
    mostrar(0);
    mostrar(NULL);
    mostrar(nullptr);
}
$ g++ -std=c++23 -Wall -Wextra sobrecarga.cpp -o sobrecarga
sobrecarga.cpp: In function ‘int main()’:
sobrecarga.cpp:9:12: error: call of overloaded ‘mostrar(NULL)’ is ambiguous
    9 |     mostrar(NULL);
      |     ~~~~~~~^~~~~~
sobrecarga.cpp:9:12: note: there are 2 candidates
sobrecarga.cpp:4:6: note: candidate 1: ‘void mostrar(int)’
    4 | void mostrar(int)  { std::cout << "chamou mostrar(int)\n"; }
      |      ^~~~~~~
sobrecarga.cpp:5:6: note: candidate 2: ‘void mostrar(int*)’
    5 | void mostrar(int*) { std::cout << "chamou mostrar(int*)\n"; }
      |      ^~~~~~~

"A chamada da função sobrecarregada mostrar(NULL) é ambígua": o NULL serve tanto para o int quanto para o int*, e o compilador não sabe qual escolher. As notas listam os dois candidatos. Apagando a linha do NULL:

$ g++ -std=c++23 -Wall -Wextra sobrecarga.cpp -o sobrecarga
$ ./sobrecarga
chamou mostrar(int)            « o 0 foi tratado como número
chamou mostrar(int*)           « o nullptr só pode ser ponteiro

Quem escreveu mostrar(0) querendo dizer "ponteiro nulo" acabou chamando a versão errada, sem erro nenhum. Com nullptr, não há dúvida possível: ele nunca é confundido com um número. Por isso a regra é simples: em C++ moderno, ponteiro nulo é sempre nullptr.


4. Sempre checar antes de usar

Ponteiros nulos aparecem muito como resposta de "procurei e não achei". A função devolve o endereço do que achou, ou nullptr se não achou, e quem chama tem a obrigação de conferir:

#include <iostream>
#include <vector>

// devolve o endereço do primeiro item negativo, ou nullptr se não houver
int* primeiro_negativo(std::vector<int>& v) {
    for (int& n : v) {
        if (n < 0) {
            return &n;
        }
    }
    return nullptr;
}

int main() {
    std::vector<int> extrato = {100, 50, -30, 20};
    std::vector<int> poupanca = {10, 20, 30};

    int* p = primeiro_negativo(extrato);
    if (p != nullptr) {
        std::cout << "Extrato: primeiro débito de " << *p << "\n";
        *p = 0;     // dá para mudar pelo ponteiro
    }

    int* q = primeiro_negativo(poupanca);
    if (q != nullptr) {
        std::cout << "Poupança: primeiro débito de " << *q << "\n";
    } else {
        std::cout << "Poupança: nenhum débito\n";
    }
}
$ g++ -std=c++23 -Wall -Wextra buscar.cpp -o buscar
$ ./buscar
Extrato: primeiro débito de -30
Poupança: nenhum débito          « q era nulo, e o else tratou o caso
int* primeiro_negativo(…)
A função devolve um int*: o endereço de um item do vetor. O vetor veio por referência (std::vector<int>&), então os itens continuam vivos depois que a função termina, e o endereço continua válido.
return &n;
Como n é uma referência para o item (veja Referências), &n é o endereço do item dentro do vetor.
return nullptr;
"Não achei". Um valor claro, que quem chama consegue testar.
if (q != nullptr) … else …
O teste obrigatório. Sem ele, *q derrubaria o programa.

Um atalho útil: o && só avalia o lado direito se o esquerdo for verdadeiro (curto-circuito). Então if (p != nullptr && *p > 100) é seguro: se p for nulo, o *p nem é executado. Invertendo a ordem (*p > 100 && p != nullptr), o programa cai antes de conferir.

Hábitos que evitam o problema

  1. Todo ponteiro nasce com valor: um endereço ou nullptr. Nunca int* p;.
  2. Função que pode devolver nullptr diz isso no comentário ou no nome (buscar…, achar…), e quem chama confere.
  3. Se "nada" não é uma resposta válida, não use ponteiro: use uma referência, que nunca é nula.
  4. Para "talvez um valor", o C++17 tem o std::optional (aula optional e variant), que não precisa de ponteiro nenhum.
  5. Depois de devolver memória do heap, o ponteiro antigo vira perigoso; a aula new e delete mostra isso, e os ponteiros inteligentes resolvem.

Quando dá errado

"Falha de segmentação" e código 139
$ ./crash
Antes
Falha de segmentação     (imagem do núcleo gravada) ./crash
$ echo $?
139

O que aconteceu: o programa acessou uma memória proibida. As causas mais comuns são: seguir um ponteiro nulo (esta aula), um ponteiro não inicializado (Ponteiros), um ponteiro para memória já devolvida (new e delete), passar do fim de um array ou estourar a pilha (Como a Memória Funciona).

Como resolver: recompile com -g -fsanitize=address e rode de novo: a linha #0 … arquivo.cpp:N do relatório aponta o lugar. Se for um ponteiro nulo (Hint: address points to the zero page), descubra de onde ele veio e coloque o teste if (p != nullptr) antes do uso. Outra opção é o GDB.

"call of overloaded … (NULL) is ambiguous"
$ g++ -std=c++23 -Wall -Wextra sobrecarga.cpp -o sobrecarga
sobrecarga.cpp: In function ‘int main()’:
sobrecarga.cpp:9:12: error: call of overloaded ‘mostrar(NULL)’ is ambiguous
    9 |     mostrar(NULL);
      |     ~~~~~~~^~~~~~

O que aconteceu: NULL é um número disfarçado, e existem versões da função para número e para ponteiro.

Como resolver: troque NULL por nullptr. Vale para todo o programa: procure NULL e substitua.

"Testei com if (p) e mesmo assim caiu"

O que aconteceu: o teste só reconhece o nullptr. Um ponteiro não inicializado (lixo) ou que aponta para memória já devolvida não é nulo: o if (p) dá verdadeiro, e o *p derruba o programa do mesmo jeito.

Como resolver: inicialize todo ponteiro com nullptr e volte a pôr nullptr quando o alvo deixar de existir. Melhor ainda: use ponteiros inteligentes, que fazem isso sozinhos.


Referência rápida

CódigoO que faz
int* p = nullptr;Cria um ponteiro que não aponta para nada.
p == nullptr / !pVerdadeiro se p for nulo.
p != nullptr / pVerdadeiro se p apontar para algo.
if (p && *p > 0)Seguro: o *p só roda se p não for nulo.
return nullptr;Jeito comum de uma função dizer "não achei".
*p com p nuloComportamento indefinido; no Linux, Falha de segmentação, código 139.
NULL, 0Formas antigas; podem ser confundidas com número. Use nullptr.
-g -fsanitize=addressNa queda, mostra arquivo e linha (SEGV on unknown address 0x000000000000).

Exercícios

1. Nasce nulo

Complete para criar um ponteiro para double que não aponta para nada:

double* p = ;

2. Conferir antes

Complete o teste para que o *p só rode quando p apontar para algo:

if (p nullptr) { std::cout << *p; }

3. Um programa terminou com Falha de segmentação e echo $? mostrou 139. O que isso indica?

4. Por que preferir nullptr a NULL?

5. Qual destes testes é seguro quando p pode ser nulo?

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

Missão: a função primeiro_acima devolve o endereço da primeira nota maior que o limite. Complete o que falta: (1) quando nenhuma nota passa do limite, a função deve devolver nullptr; (2) no main, confira q antes de usar e, se for nulo, mostre Nenhum acima de 20. A saída deve ser:

Primeiro acima de 8: 9
Nenhum acima de 20
#include <iostream>
#include <vector>

int* primeiro_acima(std::vector<int>& v, int limite) {
    for (int& n : v) {
        if (n > limite) {
            return &n;
        }
    }
    // o que devolver quando nenhum item passa do limite?
}

int main() {
    std::vector<int> notas = {7, 8, 9, 10};

    int* p = primeiro_acima(notas, 8);
    if (p != nullptr) {
        std::cout << "Primeiro acima de 8: " << *p << "\n";
    }

    int* q = primeiro_acima(notas, 20);
    // confira q antes de usar: mostre o valor ou "Nenhum acima de 20"
}
#include <iostream>
#include <vector>

int* primeiro_acima(std::vector<int>& v, int limite) {
    for (int& n : v) {
        if (n > limite) {
            return &n;
        }
    }
    return nullptr;
}

int main() {
    std::vector<int> notas = {7, 8, 9, 10};

    int* p = primeiro_acima(notas, 8);
    if (p != nullptr) {
        std::cout << "Primeiro acima de 8: " << *p << "\n";
    }

    int* q = primeiro_acima(notas, 20);
    if (q != nullptr) {
        std::cout << "Primeiro acima de 20: " << *q << "\n";
    } else {
        std::cout << "Nenhum acima de 20\n";
    }
}

Resumo

  • nullptr C++11 é o ponteiro que não aponta para nada. Use para inicializar ponteiros que ainda não têm alvo.
  • Teste com p != nullptr (ou if (p)) antes de usar *p.
  • Seguir um ponteiro nulo é comportamento indefinido: no Linux, Falha de segmentação (Segmentation fault), código 139. O -fsanitize=address mostra a linha.
  • NULL e 0 são números disfarçados e confundem a sobrecarga (call of overloaded … is ambiguous). Use sempre nullptr.
  • Funções que "procuram" podem devolver nullptr para dizer "não achei"; quem chama confere.
  • Próxima aula: const com Ponteiros e Referências.