Menu

Ponteiros em C: um guia completo com exemplos

Um ponteiro é uma variável que guarda um endereço de memória. Esta página constrói a ideia do zero - os operadores & e *, declarar e desreferenciar ponteiros, por que os tipos de ponteiro importam, e a função swap() que mostra por que ponteiros existem.

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

Toda variável do seu programa mora em algum lugar da memória, e todo "algum lugar" tem um número - seu endereço. Um ponteiro é apenas uma variável que guarda um desses números. Essa é a ideia inteira. Todo o resto desta página é sintaxe e consequências.

A razão de ponteiros importarem tanto em C é que C entrega a cada função uma cópia de seus argumentos. Uma função que recebe o número 7 pode mudar sua cópia à vontade; a variável de quem chamou nunca se move. Dê à função um endereço em vez disso, e ela pode alcançar de volta e mudar o original. Arrays, strings, memória dinâmica e toda estrutura de dados que você algum dia construirá em C se apoiam nessa única capacidade.

Memória, endereços e por que tudo tem um

Quando você escreve int score = 42;, o compilador reserva alguns bytes de memória e dá a eles um nome que você pode usar. Esses bytes também têm um endereço numérico. Pense na memória como uma rua muito longa de casas numeradas: score é a família morando na casa de número 0x7ffd4c2a - o nome é para você, o número é para a máquina.

O operador & pede esse número:

Execute. Os três endereços são diferentes porque as três variáveis ocupam bytes diferentes, e eles vão mudar de execução para execução - isso é normal, e é por isso que você nunca fixa um endereço no código. Note a conversão (void *): %p é especificado para receber um void *, e passar qualquer outro tipo de ponteiro é tecnicamente comportamento indefinido. Faça da conversão um hábito.

Os endereços serem imprevisíveis não é um problema, porque você quase nunca se importa com o que um endereço é. Você se importa em tê-lo.

Declarando um ponteiro

Uma declaração de ponteiro nomeia o tipo para o qual ele aponta:

int    *p;      // p pode guardar o endereço de um int
double *q;      // q pode guardar o endereço de um double
char   *name;   // name pode guardar o endereço de um char

Leia int *p; como "*p é um int" - desreferenciar p te dá um int. Essa leitura sobrevive a toda declaração complicada que C puder te jogar mais adiante.

Onde você coloca o * é uma escolha de estilo que o compilador ignora: int* p, int *p e int * p declaram todos a mesma coisa. A maior parte do código C escreve int *p, e há uma razão prática:

int* a, b;   // a é um ponteiro para int; b é um int COMUM, não um ponteiro
int *a, *b;  // os dois são ponteiros - o * se liga ao nome, não ao tipo

O * se prende ao declarador, não ao tipo. Escrevê-lo ao lado do nome mantém isso visível.

& e *: os dois operadores

Dois operadores fazem todo o trabalho, e eles são exatamente opostos.

  • &x - endereço de. Produz o endereço onde x mora.
  • *p - desreferência. Vai até o endereço em p e te dá o objeto que mora lá.

As duas últimas linhas são todo o ponto dos ponteiros. Nada foi atribuído a age diretamente, e ainda assim age mudou - porque *p = 31 significa "guarde 31 no endereço que p contém", e esse endereço é o endereço de age.

Eis a figura na memória:

      p                        age
 +------------+           +----------+
 | 0x7ffd1c40 | --------> |    31    |
 +------------+           +----------+
 em 0x7ffd1c38            em 0x7ffd1c40

 p    -> o endereço 0x7ffd1c40
 *p   -> o int guardado lá, 31
 &age -> 0x7ffd1c40, o mesmo endereço que p contém
 &p   -> 0x7ffd1c38, onde o próprio p mora

Note a última linha: o ponteiro é ele mesmo uma variável em algum lugar da memória, então ele tem seu próprio endereço também. Isso não é pegadinha, é apenas coerência - e é assim que ponteiros para ponteiros (int **) funcionam.

Como & e * desfazem um ao outro, *&age é simplesmente age, e &*p é simplesmente p.

Por que os tipos de ponteiro importam

Um ponteiro guarda um endereço, e um endereço é apenas um número - então por que o compilador insiste em saber se ele aponta para um int ou um double? Duas razões:

  1. Quantos bytes ler. Desreferenciar um int * lê 4 bytes (tipicamente); desreferenciar um double * lê 8. Sem o tipo, *p não teria significado.
  2. Como interpretar esses bytes. O mesmo padrão de bits é um número como int e um completamente diferente como float.

O tamanho do ponteiro em si, por outro lado, não depende daquilo para o que ele aponta - um endereço é um endereço:

Em uma máquina de 64 bits os três ponteiros têm 8 bytes, enquanto as coisas para as quais apontam têm 4, 8 e 1. O tipo apontado é o que faz *p e a aritmética de ponteiros significarem alguma coisa.

Atribuir entre tipos de ponteiro diferentes é um erro do qual o compilador vai reclamar, e por boa razão - pi = &d; te prepararia para ler 4 bytes de um double e chamar o resultado de inteiro.

A motivação canônica: swap()

Eis a função que todo curso de C usa, porque ela falha exatamente do jeito que explica ponteiros.

A função relata que os trocou - e main relata que nada aconteceu. broken_swap recebeu cópias de x e y. Ela trocou suas cópias perfeitamente, e então essas cópias deixaram de existir quando a função retornou.

Entregue os endereços no lugar:

Agora a e b continuam sendo cópias - cópias de dois endereços - mas uma cópia de um endereço aponta para a mesmíssima variável para a qual o original apontava. *a é x, de onde quer que a função seja chamada.

Esse é o padrão por trás de toda função C que modifica os dados de quem a chamou, e por trás dos parâmetros de função em geral: C não tem passagem por referência, então você passa um ponteiro e o desreferencia. É também por isso que scanf precisa de um &:

int n;
scanf("%d", &n);   // scanf precisa poder escrever em n, então precisa do endereço de n

Devolvendo mais de um valor

Uma função C devolve um valor. Ponteiros são como você contorna isso: passe endereços para os resultados extras.

O valor de retorno carrega o status e os ponteiros carregam os resultados. Essa divisão está por toda parte na própria biblioteca padrão de C e em APIs de sistema operacional.

Ponteiros para ponteiros

Como um ponteiro é uma variável, você pode tomar o endereço dele, e o tipo desse endereço é "ponteiro para ponteiro para int":

  pp              p               value
+------+       +------+        +------+
| &p   | ----> | &val | -----> |   8  |
+------+       +------+        +------+

Você vai encontrar ** de verdade quando uma função precisar mudar um ponteiro que quem a chamou possui - por exemplo, um alocador que aponta o ponteiro de quem chamou para memória nova - e no char *argv[] dos argumentos de linha de comando.

Quatro erros que vale conhecer antes de cometê-los

Usar um ponteiro que nunca foi apontado para lugar nenhum. Um ponteiro não inicializado guarda quaisquer bytes que já estivessem ali. Desreferenciá-lo lê ou escreve em um endereço aleatório.

int *p;      // p guarda lixo
*p = 10;     // comportamento indefinido - provavelmente uma falha

Inicialize todo ponteiro, com um endereço real ou com NULL. Veja ponteiros nulos para a disciplina em torno disso.

Confundir *p com p. p = 5; define o ponteiro para o endereço 5 (um absurdo); *p = 5; guarda 5 onde ele aponta. O compilador vai avisar no primeiro caso, e avisos valem a pena ser lidos.

Esquecer o & no scanf. scanf("%d", n) passa o valor de n como endereço. Compila com um aviso e depois escreve em qualquer memória que esse número nomeie.

Devolver o endereço de uma variável local. A local some no momento em que a função retorna; o endereço é um recibo de um imóvel demolido.

int *bad(void) {
    int local = 42;
    return &local;   // quem chamou recebe um ponteiro pendurado
}

Se você precisa de memória que sobreviva à chamada, aloque-a - é para isso que serve a memória dinâmica.

Para onde ir a seguir

Ponteiros não param em "guardar um endereço". Somar a um ponteiro o faz caminhar pela memória em unidades do seu tipo, que é o assunto da aritmética de ponteiros; e um nome de array em C decai para um ponteiro ao seu primeiro elemento, e é por isso que ponteiros e arrays são efetivamente o mesmo assunto depois que você conhece os dois. Leia esses dois a seguir, e o resto de C para de parecer mágica.

Perguntas frequentes

O que é um ponteiro em C?

Um ponteiro é uma variável cujo valor é um endereço de memória - a localização de algum outro objeto na memória. int *p = &age; faz p guardar o endereço de age, e *p então lê ou escreve na variável age através desse endereço.

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

&x é o operador de endereço: ele produz o endereço onde x mora. *p é o operador de desreferência: ele vai até o endereço guardado em p e te dá o objeto que está lá. Um desfaz o outro, então *&x é simplesmente x.

Por que o * aparece duas vezes - na declaração e ao usar o ponteiro?

São dois usos diferentes do mesmo símbolo. Em int *p; o * faz parte do tipo, dizendo "p é um ponteiro para int". Em *p = 5; o * é o operador de desreferência, dizendo "guarde 5 no endereço que p contém". Ler a declaração como "*p é um int" faz os dois fazerem sentido.

Por que eu preciso de ponteiros em C?

C passa todo argumento por valor, então uma função nunca pode mudar a variável de quem a chamou a menos que receba o endereço. Ponteiros também deixam você percorrer arrays de forma eficiente, devolver mais de um resultado, construir estruturas encadeadas e usar memória alocada em tempo de execução com malloc.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR