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.
- 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,NULLe0, 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 papel | Em C++ | Dá para ir até a casa? |
|---|---|---|
| Com um endereço válido anotado | int* p = &x; | Sim: *p é o x. |
| Escrito "nenhum endereço", bem legível | int* p = nullptr; | Não, e você consegue saber que não dá. |
| Rabiscado, ilegível | int* 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
- "
paponta para alguma coisa?" Só dentro desseifé 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 longa | Forma curta | Pergunta |
|---|---|---|
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.
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":
| Forma | De onde vem | O que é, de verdade | Use? |
|---|---|---|---|
nullptr | C++11 (2011) | Um valor especial, do tipo std::nullptr_t, que só combina com ponteiros. | Sim, sempre. |
NULL | Herdado 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. |
0 | C e C++ antigos | O 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,
*qderrubaria 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
- Todo ponteiro nasce com valor: um endereço ou
nullptr. Nuncaint* p;. - Função que pode devolver
nullptrdiz isso no comentário ou no nome (buscar…,achar…), e quem chama confere. - Se "nada" não é uma resposta válida, não use ponteiro: use uma referência, que nunca é nula.
- Para "talvez um valor", o C++17 tem o
std::optional(aula optional e variant), que não precisa de ponteiro nenhum. - 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
$ ./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.
$ 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.
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ódigo | O que faz |
|---|---|
int* p = nullptr; | Cria um ponteiro que não aponta para nada. |
p == nullptr / !p | Verdadeiro se p for nulo. |
p != nullptr / p | Verdadeiro 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 nulo | Comportamento indefinido; no Linux, Falha de segmentação, código 139. |
NULL, 0 | Formas antigas; podem ser confundidas com número. Use nullptr. |
-g -fsanitize=address | Na 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:
2. Conferir antes
Complete o teste para que o *p só rode quando p apontar para algo:
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
nullptrC++11 é o ponteiro que não aponta para nada. Use para inicializar ponteiros que ainda não têm alvo.- Teste com
p != nullptr(ouif (p)) antes de usar*p. - Seguir um ponteiro nulo é comportamento indefinido: no Linux,
Falha de segmentação(Segmentation fault), código 139. O-fsanitize=addressmostra a linha. NULLe0são números disfarçados e confundem a sobrecarga (call of overloaded … is ambiguous). Use semprenullptr.- Funções que "procuram" podem devolver
nullptrpara dizer "não achei"; quem chama confere. - Próxima aula: const com Ponteiros e Referências.