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 ondexmora.*p- desreferência. Vai até o endereço empe 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:
- Quantos bytes ler. Desreferenciar um
int *lê 4 bytes (tipicamente); desreferenciar umdouble *lê 8. Sem o tipo,*pnão teria significado. - Como interpretar esses bytes. O mesmo padrão de bits é um número como
inte um completamente diferente comofloat.
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.