Menu

Ponteiros em C++: operador de endereço, dereferência e nullptr

Ponteiros em C++ explicados do zero: declarar um ponteiro, os operadores & (endereço de) e * (dereferência), nullptr, ponteiros para arrays e as armadilhas de ponteiros pendentes e não inicializados que causam travamentos.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Uma variável que guarda um endereço

Toda variável vive em algum lugar da memória, em uma posição numerada chamada de seu endereço. Na maior parte do tempo você não se importa com onde: simplesmente usa o nome da variável. Um ponteiro inverte isso: é uma variável cujo valor é um endereço. Em vez de guardar 42, ele guarda "o lugar onde 42 está armazenado".

Essa indireção é o que torna os ponteiros poderosos. Funções podem alterar uma variável de quem as chama por meio de um, estruturas de dados como listas encadeadas encadeiam nós com eles e (como você verá em memória dinâmica) são a forma de acessar a memória que você aloca em tempo de execução.

O & em &score é o operador endereço de: ele produz a localização de score. O * em *p é o operador dereferência: ele segue o endereço de volta até o valor que vive ali.

Os dois operadores: & e *

A coisa mais confusa para iniciantes é que * significa duas coisas diferentes dependendo de onde aparece. Mantenha isto claro:

int* p;     // DECLARAÇÃO: "p é um ponteiro para int"
p = &x;     // & = endereço de: guarda o endereço de x em p
int y = *p; // * = dereferência: lê o valor para o qual p aponta
*p = 99;    // dereferência à esquerda: escreve através do ponteiro

Em uma declaração, * faz parte do tipo. Em uma expressão, * realiza trabalho. Uma vez que um ponteiro está configurado, dereferenciá-lo dá acesso total de leitura/escrita à variável original:

Repare que você nunca tocou em health pelo nome depois da linha 1, e mesmo assim seu valor continuou mudando. Esse é o ponto principal: hp é um apelido para o mesmo armazenamento. O espaçamento (int* p, int *p, int*p) é cosmético e idêntico para o compilador; este guia usa int* p.

nullptr: apontar para nada

Um ponteiro que não aponta para lugar nenhum deve ser definido como nullptr (C++11). É uma forma clara e com segurança de tipos de dizer "ainda sem alvo", e dá a você algo para testar antes de dereferenciar.

Prefira nullptr à antiga macro NULL ou a um 0 puro. Como nullptr tem um tipo de ponteiro real, ele nunca é mal interpretado como o inteiro 0 durante a resolução de sobrecargas, um bug sutil que o estilo antigo podia causar.

Armadilha: a dereferência nula. Ler ou escrever através de um ponteiro nulo (ou não inicializado) é comportamento indefinido, geralmente um travamento instantâneo:

int* p = nullptr;
cout << *p;   // TRAVAMENTO - dereferenciar nulo é comportamento indefinido

Sempre proteja com if (p) (ou if (p != nullptr)) antes de dereferenciar qualquer coisa que possa ser nula.

Ponteiros e arrays

O nome de um array decai para um ponteiro ao seu primeiro elemento, então ponteiros e arrays estão profundamente entrelaçados. Somar 1 a um ponteiro não adiciona um byte: ele avança um elemento, que é o que faz a aritmética de ponteiros funcionar:

p[i] e *(p + i) são literalmente a mesma expressão; essa equivalência é a razão de os arrays serem indexados a partir de zero. O bug clássico aqui é passar do fim: nums + 4 é um marcador válido de um-além-do-fim para comparar, mas dereferenciar *(nums + 4) lê fora dos limites. Erros de um a mais com ponteiros são uma das principais causas de travamentos e corrupção silenciosa, então seja cuidadoso com sua condição de parada.

const e ponteiros

const pode se aplicar àquilo para onde o ponteiro aponta, ao próprio ponteiro, ou a ambos. Leia a declaração da direita para a esquerda para decifrá-la:

const int* p;        // ponteiro para const int  - não dá para mudar *p, dá para reapontar p
int* const p = &x;   // ponteiro const para int  - dá para mudar *p, não dá para reapontar p
const int* const p = &x; // ambos travados

Isso importa o tempo todo no código real. Uma função que promete não modificar seus dados recebe um ponteiro para const:

Marcar como const aquilo para onde se aponta documenta a intenção e permite ao compilador impedir escritas acidentais: segurança de graça e sem custo em tempo de execução.

A grande armadilha: ponteiros pendentes

Um ponteiro pendente aponta para memória que não contém mais o valor que você espera: a variável saiu de escopo, ou a memória foi liberada. Dereferenciá-lo é comportamento indefinido, e o pior é que muitas vezes parece funcionar até que para de funcionar.

int* makeBad() {
    int local = 5;
    return &local;   // BUG: local morre quando a função retorna
}                    // o ponteiro retornado agora fica pendente

O endereço continua sendo um número válido, mas aponta para um espaço da pilha que já foi recuperado: lê-lo dá lixo ou trava. A mesma coisa acontece se você mantiver um ponteiro para um objeto do heap que sofreu delete ou para um elemento de um vector que depois realoca.

Três regras mantêm você seguro:

  • Nunca retorne o endereço de uma variável local. Retorne por valor, ou faça com que quem chama seja dono do armazenamento.
  • Defina um ponteiro como nullptr depois que aquilo para onde ele aponta deixar de existir, e verifique antes de usar.
  • Para posse e tempos de vida, recorra aos ponteiros inteligentes em vez de new/delete crus: eles liberam a memória automaticamente e reduzem toda essa classe de bugs.

A seguir: referências versus ponteiros

Ponteiros não são a única forma de se referir indiretamente a outra variável. C++ também tem referências, que parecem similares mas não podem ser nulas, não podem ser reapontadas e usam uma sintaxe mais limpa. A seguir vamos colocá-las lado a lado em referências versus ponteiros para que você saiba exatamente qual ferramenta escolher, e por que a maior parte do C++ moderno prefere referências quando pode usá-las.

Perguntas frequentes

O que é um ponteiro em C++?

Um ponteiro é uma variável que armazena o endereço de memória de outro valor, em vez do valor em si. Você o declara com * (por exemplo, int* p), obtém um endereço com o operador & (p = &x) e lê ou escreve o valor para o qual ele aponta dereferenciando com *p.

Qual é a diferença entre & e * nos ponteiros em C++?

Em um contexto de ponteiros, & é o operador endereço de: &x fornece o endereço de x. * faz dois trabalhos: em uma declaração (int* p) ele marca a variável como ponteiro, e em uma expressão (*p) ele dereferencia o ponteiro para chegar ao valor armazenado nesse endereço.

O que é nullptr em C++ e por que usá-lo em vez de NULL?

nullptr é um literal de ponteiro nulo com segurança de tipos, adicionado no C++11. Significa "não aponta para nada". Prefira-o ao antigo NULL ou 0 porque nullptr tem um tipo de ponteiro real, então ele nunca é confundido com um inteiro durante a resolução de sobrecargas. Sempre verifique if (p) antes de dereferenciar: dereferenciar um ponteiro nulo é comportamento indefinido.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR