C Ponteiros
Na aula Memória e Endereços você viu que toda variável mora numa gaveta numerada, e que &x mostra o número dessa gaveta. Um ponteiro é só uma variável que guarda esse número. Parece pouco, e é: a ideia inteira cabe numa frase. O que confunde é o símbolo *, que aparece em dois papéis diferentes. Esta aula vai devagar, uma novidade por vez, com desenhos das gavetas em cada passo.
- O que é um ponteiro, com uma comparação do dia a dia.
- Como declarar um ponteiro (
int *p) e fazê-lo apontar para uma variável (p = &x). - Como ler e escrever na variável apontada com
*p. - A diferença entre o
*da declaração e o*do uso. - Por que o tipo do ponteiro importa.
- Por que um ponteiro sem valor inicial é perigoso, com o aviso real do compilador e o programa morrendo.
Todos os programas foram compilados na máquina da Ana (um Linux de 64 bits), na pasta ~/c, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa. Lembre: os endereços mudam a cada execução (ASLR); os seus serão outros.
A ideia: um bilhete com o número da gaveta
A Ana guarda as economias numa gaveta do arquivo do escritório, a gaveta 1000. Ela não quer espalhar o dinheiro, mas quer que o irmão consiga colocar mais dinheiro lá. Então ela escreve num bilhete: "gaveta 1000". O bilhete não tem dinheiro nenhum: tem só o número da gaveta onde o dinheiro está. Com o bilhete na mão, o irmão pode:
- olhar quanto tem na gaveta 1000;
- colocar um valor novo na gaveta 1000;
- apagar o bilhete e escrever outro número nele, para cuidar de outra gaveta.
Em C, o bilhete é o ponteiro. E o bilhete também é guardado numa gaveta: um ponteiro é uma variável como as outras, só que o que mora dentro dela é um endereço.
| Na história | Em C |
|---|---|
| A gaveta com o dinheiro | Uma variável comum: int economias = 50; |
| O bilhete com "gaveta 1000" | Um ponteiro: int *p = &economias; |
| Ir até a gaveta escrita no bilhete e olhar | Ler pelo ponteiro: *p |
| Ir até a gaveta e trocar o conteúdo | Escrever pelo ponteiro: *p = 80; |
| Escrever outro número no bilhete | Apontar para outra variável: p = &outra; |
1. Criar um ponteiro
O primeiro programa cria uma variável comum, pontos, e um ponteiro, p, que guarda o endereço dela:
#include <stdio.h>
int main(void) {
int pontos = 10;
int *p = &pontos;
printf("pontos vale %d e mora em %p\n", pontos, (void *) &pontos);
printf("p guarda %p\n", (void *) p);
printf("*p vale %d\n", *p);
printf("p mora em %p\n", (void *) &p);
return 0;
}
$ gcc -std=c23 -Wall -Wextra ponteiro.c -o ponteiro $ ./ponteiro pontos vale 10 e mora em 0x7ffdcfd4b7ec p guarda 0x7ffdcfd4b7ec « o mesmo número: o endereço de pontos *p vale 10 « seguindo o endereço, chega-se ao 10 p mora em 0x7ffdcfd4b7f0 « o próprio p tem a sua gaveta
A linha nova é uma só. Vamos desmontá-la:
- int
- O tipo da variável apontada: "este ponteiro vai apontar para um
int". - *
- Na declaração, o
*quer dizer "é um ponteiro". Junto com oint, forma o tipoint *, que se lê "ponteiro paraint". - p
- O nome do ponteiro.
pé comum em exemplos curtos; em programas de verdade, prefira nomes que digam para onde ele aponta, comop_pontosouatual. - = &pontos
- O valor inicial: o endereço de
pontos. A partir daqui, dizemos que "paponta parapontos".
E nas linhas do printf:
psozinho é o conteúdo do bilhete: o endereço guardado. Para mostrar,%pcom(void *), como qualquer endereço.*pé "vá até o endereço que está empe pegue o que tem lá". Isso se chama desreferenciar o ponteiro (seguir a referência). Como lá está umint, o formato é%d.&pé o endereço do próprio ponteiro. O bilhete também está guardado em algum lugar.
pontos está o 10. Dentro de p não está o 10: está o número da gaveta de pontos. A seta é só o desenho dessa ligação; no computador, ela é aquele número.Em forma de tabela "endereço → valor", do jeito que o computador enxerga:
| Endereço | Nome | Tipo (tamanho) | O que está guardado |
|---|---|---|---|
0x7ffdcfd4b7ec | pontos | int (4 bytes) | 10 |
0x7ffdcfd4b7f0 | p | int * (8 bytes) | 0x7ffdcfd4b7ec |
int *p = &pontos; → "p é um ponteiro para int que recebe o endereço de pontos".*p → "o valor apontado por p" ou "conteúdo de p".
2. Escrever pelo ponteiro
O *p não serve só para ler. Do lado esquerdo do =, ele quer dizer "guarde este valor na gaveta para onde p aponta". A variável pontos muda sem que o nome dela apareça na linha:
#include <stdio.h>
int main(void) {
int pontos = 10;
int *p = &pontos;
*p = 25;
printf("pontos = %d\n", pontos);
*p = *p + 5;
printf("pontos = %d\n", pontos);
pontos = 100;
printf("*p = %d\n", *p);
return 0;
}
$ gcc -std=c23 -Wall -Wextra escrever.c -o escrever $ ./escrever pontos = 25 « *p = 25 escreveu dentro de pontos pontos = 30 « leu 25 pelo ponteiro, somou 5, escreveu 30 *p = 100 « mudou pontos pelo nome; o ponteiro vê a mudança
| Linha | O que acontece | pontos |
|---|---|---|
int pontos = 10; | Cria a gaveta com 10. | 10 |
int *p = &pontos; | Cria o bilhete com o endereço de pontos. | 10 |
*p = 25; | Vai até a gaveta anotada em p e guarda 25. | 25 |
*p = *p + 5; | Lê o que tem lá (25), soma 5, guarda lá (30). | 30 |
pontos = 100; | Muda pelo nome. p continua apontando para a mesma gaveta. | 100 |
Não existem duas cópias do número: existe uma gaveta só, com dois jeitos de chegar até ela, pelo nome (pontos) e pelo endereço (*p). Por isso, mudar por um caminho aparece no outro. *p += 5; e (*p)++; também funcionam (os parênteses do (*p)++ são necessários; veja Precedência).
Missão: o ponteiro p já aponta para saldo. Usando o p (com *p = …), mude o saldo para 100. O printf mostra a variável saldo, que precisa mudar.
saldo = 100
#include <stdio.h>
int main(void) {
int saldo = 50;
int *p = &saldo;
// use o ponteiro p para guardar 100 em saldo
printf("saldo = %d\n", saldo);
return 0;
}
#include <stdio.h>
int main(void) {
int saldo = 50;
int *p = &saldo;
*p = 100;
printf("saldo = %d\n", saldo);
return 0;
}
3. Apontar para outra variável
O bilhete pode ser reescrito. Sem o *, uma atribuição muda o próprio ponteiro (o número anotado), e não a variável apontada:
#include <stdio.h>
int main(void) {
int ana = 7;
int bia = 9;
int *p = &ana;
printf("p aponta para ana: *p = %d\n", *p);
p = &bia;
printf("p aponta para bia: *p = %d\n", *p);
*p = 10;
printf("ana = %d, bia = %d\n", ana, bia);
return 0;
}
$ gcc -std=c23 -Wall -Wextra trocar-alvo.c -o trocar-alvo $ ./trocar-alvo p aponta para ana: *p = 7 p aponta para bia: *p = 9 ana = 7, bia = 10 « o *p = 10 foi para a bia, a gaveta atual do bilhete
p = &bia; e *p = 10;. A ana não mudou: o bilhete não fala mais dela.Guarde a regra, que é o coração desta aula:
| Você escreve | Muda o quê | Na história |
|---|---|---|
p = &bia; | O ponteiro (para onde ele aponta) | Reescreve o número no bilhete |
*p = 10; | A variável apontada | Vai até a gaveta e troca o conteúdo |
4. O * da declaração × o * do uso
Esta é a confusão número um dos iniciantes, então vale uma seção só para ela. O mesmo símbolo * tem dois trabalhos, conforme o lugar:
| Onde aparece | Exemplo | Quer dizer |
|---|---|---|
| Numa declaração (com um tipo na frente) | int *p = &x; | "p é um ponteiro para int". O = &x inicializa o p, e não o *p. |
| No uso (sem tipo na frente) | *p = 5; ou printf("%d", *p); | "A variável para onde p aponta". Desreferenciar. |
| Entre dois valores | a * b | Multiplicação, como sempre. |
A primeira linha engana: int *p = &x; parece dizer "*p recebe &x", mas quer dizer "p recebe &x". Ela é o atalho das duas linhas abaixo:
int *p; // declara: p é um ponteiro para int p = &x; // usa: guarda em p o endereço de x (sem *)
Colocar o * na segunda linha é um erro comum, e o compilador recusa:
#include <stdio.h>
int main(void) {
int pontos = 10;
int *p;
*p = &pontos;
printf("%d\n", *p);
return 0;
}
$ gcc -std=c23 -Wall -Wextra estrela.c -o estrela
estrela.c: In function ‘main’:
estrela.c:7:8: error: assignment to ‘int’ from ‘int *’ makes integer from pointer without a cast [-Wint-conversion]
7 | *p = &pontos;
| ^
"Atribuição a um int a partir de um int * transforma ponteiro em inteiro sem conversão". Traduzindo: *p é um int (a gaveta apontada), e você tentou guardar nele um endereço. O conserto é tirar o *: p = &pontos;.
Onde fica o espaço: int *p, int* p ou int * p?
Para o compilador, as três formas são iguais. O costume mais comum em C (e o que este guia usa) é int *p, grudado no nome, por causa de uma armadilha: numa declaração com vírgula, o * vale só para o nome ao lado dele.
#include <stdio.h>
int main(void) {
int a = 1, b = 2;
int *p, q;
p = &a;
q = &b;
printf("%d %d\n", *p, *q);
return 0;
}
$ gcc -std=c23 -Wall -Wextra virgula.c -o virgula
virgula.c: In function ‘main’:
virgula.c:8:7: error: assignment to ‘int’ from ‘int *’ makes integer from pointer without a cast [-Wint-conversion]
8 | q = &b;
| ^
virgula.c:9:27: error: invalid type argument of unary ‘*’ (have ‘int’)
9 | printf("%d %d\n", *p, *q);
| ^~
A linha int *p, q; criou p como ponteiro e q como int comum. Por isso q não pode guardar endereço, e *q não faz sentido ("argumento de tipo inválido para o *: é um int"). Escrevendo int* p, q;, o erro seria o mesmo, mas a aparência engana ainda mais. Para dois ponteiros: int *p, *q;. Melhor ainda: um ponteiro por linha.
5. O tipo do ponteiro importa
Todo endereço é só um número, então por que existe int *, double *, char *? Porque o endereço diz só onde a variável começa. Para ler *p, o programa precisa saber quantas gavetas ler a partir dali e como interpretá-las: 4 gavetas de int? 8 gavetas de double? 1 de char? É o tipo do ponteiro que diz.
#include <stdio.h>
int main(void) {
int pontos = 10;
double *p = &pontos;
printf("%f\n", *p);
return 0;
}
$ gcc -std=c23 -Wall -Wextra tipo.c -o tipo
tipo.c: In function ‘main’:
tipo.c:5:17: error: initialization of ‘double *’ from incompatible pointer type ‘int *’ [-Wincompatible-pointer-types]
5 | double *p = &pontos;
| ^
"Inicialização de um double * a partir de um tipo de ponteiro incompatível, int *". Se o compilador deixasse, o *p leria 8 gavetas a partir de pontos: as 4 do int e mais 4 de seja lá o que estiver depois, e ainda interpretaria tudo como double. Sairia lixo.
incompatible pointer types initializing 'double *' with an expression of type 'int *'. Em cursos e tutoriais antigos, você pode ver esse código "funcionando" com um aviso. Não confie nele.
Já o tamanho do ponteiro não depende do tipo apontado: char *, int * e double * ocupam todos 8 bytes no Linux de 64 bits (4 no editor), porque todos guardam a mesma coisa, um número de gaveta. Você viu isso no fim da aula Memória e Endereços.
6. O ponteiro não inicializado: um bilhete com lixo
Uma variável local criada sem valor tem lixo: o que sobrou naquela gaveta de antes. Com um int, o lixo é um número qualquer, o que já é ruim. Com um ponteiro, é pior: o lixo é um endereço qualquer. É um bilhete com um número de gaveta rabiscado ao acaso. Escrever por ele é guardar um valor numa gaveta que pode ser de qualquer um:
#include <stdio.h>
int main(void) {
int *p;
printf("Vou escrever 5 onde p aponta...\n");
*p = 5;
printf("Escrevi!\n");
return 0;
}
Primeiro, o compilador avisa (mais um motivo para nunca compilar sem -Wall):
$ gcc -std=c23 -Wall -Wextra lixo.c -o lixo
lixo.c: In function ‘main’:
lixo.c:7:8: warning: ‘p’ is used uninitialized [-Wuninitialized]
7 | *p = 5;
| ~~~^~~
lixo.c:4:10: note: ‘p’ was declared here
4 | int *p;
| ^
"p é usado sem ter sido inicializado", e a nota mostra onde ele foi declarado. É só um aviso: o programa foi gerado. Rodando:
$ ./lixo Vou escrever 5 onde p aponta... Falha de segmentação (imagem do núcleo gravada) ./lixo $ echo $? 139
O Escrevi! nunca apareceu. O endereço de lixo caiu numa região que não pertence ao programa, e o sistema o matou na hora com uma falha de segmentação (em inglês, segmentation fault, ou "segfault"). O código de saída 139 é a marca disso. No editor do guia, este programa também termina com Falha de segmentação (imagem do núcleo gravada) e [código de saída: 139], e o Clang avisa antes: variable 'p' is uninitialized when used here.
*p = 5 funcionaria "em silêncio" e estragaria outra variável qualquer. O programa seguiria rodando com um valor errado, e o defeito apareceria muito longe dali, em outro dia, em outro computador. Por isso: todo ponteiro nasce apontando para algum lugar, ou nasce nulo, que é o assunto da próxima aula, Ponteiro Nulo.
Formas de declarar: qual usar
| Forma | Comentário | Use? |
|---|---|---|
int *p = &x; | Declara e já aponta. O ponteiro nunca existe com lixo. | Sim, sempre que já souber para onde apontar. |
int *p = NULL; | Declara "apontando para nada", de propósito. Dá para testar antes de usar. | Sim, quando ainda não souber (veja Ponteiro Nulo). |
int *p; e depois p = &x; | Funciona, mas entre as duas linhas o ponteiro tem lixo. | Só se o p = &x vier logo em seguida. |
int* p; ou int * p; | Iguais a int *p para o compilador. Vários projetos usam int*. | Siga o estilo do projeto. Este guia usa int *p. |
int *p, *q; | Dois ponteiros numa linha (cada nome com o seu *). | Pode, mas um por linha é mais claro. |
Quando dá errado
&
#include <stdio.h>
int main(void) {
int pontos = 10;
int *p;
p = pontos;
printf("%d\n", *p);
return 0;
}
$ gcc -std=c23 -Wall -Wextra int2ptr.c -o int2ptr
int2ptr.c: In function ‘main’:
int2ptr.c:7:7: error: assignment to ‘int *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]
7 | p = pontos;
| ^
O que significa: "atribuição a um int * a partir de um int transforma inteiro em ponteiro sem conversão". Você tentou anotar no bilhete o conteúdo da gaveta (10), e não o número dela. O 10 viraria o endereço 10, que não é de ninguém.
Como consertar: p = &pontos;. O Clang do editor até sugere: incompatible integer to pointer conversion assigning to 'int *' from 'int'; take the address with & ("pegue o endereço com &").
* sobrando
$ gcc -std=c23 -Wall -Wextra estrela.c -o estrela
estrela.c: In function ‘main’:
estrela.c:7:8: error: assignment to ‘int’ from ‘int *’ makes integer from pointer without a cast [-Wint-conversion]
7 | *p = &pontos;
| ^
O que significa: o contrário do anterior. *p é a gaveta apontada (um int), e você tentou guardar nela um endereço.
Como consertar: para mudar para onde o ponteiro aponta, escreva sem *: p = &pontos;. O * só vai na frente quando você quer mexer na variável apontada.
$ gcc -std=c23 -Wall -Wextra tipo.c -o tipo
tipo.c: In function ‘main’:
tipo.c:5:17: error: initialization of ‘double *’ from incompatible pointer type ‘int *’ [-Wincompatible-pointer-types]
5 | double *p = &pontos;
| ^
O que significa: um ponteiro para double não pode receber o endereço de um int.
Como consertar: o tipo antes do * tem de ser o tipo da variável: int *p = &pontos;.
$ gcc -std=c23 -Wall -Wextra lixo.c -o lixo
lixo.c: In function ‘main’:
lixo.c:7:8: warning: ‘p’ is used uninitialized [-Wuninitialized]
7 | *p = 5;
| ~~~^~~
lixo.c:4:10: note: ‘p’ was declared here
4 | int *p;
| ^
$ ./lixo
Vou escrever 5 onde p aponta...
Falha de segmentação (imagem do núcleo gravada) ./lixo
O que significa: o ponteiro foi usado com lixo dentro. Esse aviso nunca deve ser ignorado.
Como consertar: dê um valor ao ponteiro na mesma linha em que ele nasce: int *p = &alguma_variavel; ou int *p = NULL;.
Referência rápida
| Código | O que faz |
|---|---|
int *p; | Declara p, um ponteiro para int (sem valor: lixo). |
int *p = &x; | Declara e já faz p apontar para x. |
p = &y; | Faz p apontar para outra variável. |
*p | A variável apontada (desreferenciar). Lê: printf("%d", *p). |
*p = 5; | Escreve 5 na variável apontada. |
(*p)++; / *p += 5; | Muda a variável apontada. |
printf("%p", (void *) p) | Mostra o endereço guardado em p. |
&p | O endereço do próprio ponteiro. |
int *p, *q; | Dois ponteiros (cada um com o seu *). |
sizeof(int *) | 8 no Linux de 64 bits; 4 no editor. Igual para qualquer tipo apontado. |
Exercícios
1. Declarar
Complete para criar um ponteiro pd que aponta para a variável double altura:
2. Desreferenciar
O ponteiro p aponta para vidas. Complete para guardar 3 em vidas usando o ponteiro:
3. Apontar
Complete para fazer o ponteiro p apontar para a variável nota:
4. Depois de int x = 4; int *p = &x; *p = 9;, quanto vale x?
5. Em int *a, b;, o que é b?
6. Qual linha muda para onde o ponteiro p aponta?
Missão final: um bilhete, dois cofrinhos
Missão: a Ana e a Bia têm um cofrinho cada. O ponteiro p começa apontando para o da Ana. Usando só o p: some 10 ao cofrinho da Ana; depois faça p apontar para o cofrinho da Bia e some 10 a ele também.
Ana: 60 Bia: 90
#include <stdio.h>
int main(void) {
int cofre_ana = 50;
int cofre_bia = 80;
int *p = &cofre_ana;
// 1. some 10 ao cofrinho para onde p aponta
// 2. faça p apontar para cofre_bia
// 3. some 10 de novo, pelo p
printf("Ana: %d\n", cofre_ana);
printf("Bia: %d\n", cofre_bia);
return 0;
}
#include <stdio.h>
int main(void) {
int cofre_ana = 50;
int cofre_bia = 80;
int *p = &cofre_ana;
*p = *p + 10;
p = &cofre_bia;
*p = *p + 10;
printf("Ana: %d\n", cofre_ana);
printf("Bia: %d\n", cofre_bia);
return 0;
}
Resumo
- Um ponteiro é uma variável que guarda um endereço: um bilhete com o número de uma gaveta.
int *p = &x;cria um ponteiro paraintque aponta parax.*pé a variável apontada: serve para ler (printf("%d", *p)) e para escrever (*p = 5;).p = &y;(sem*) muda para onde o ponteiro aponta;*p = …muda a variável apontada.- Na declaração,
*quer dizer "é ponteiro"; no uso, "siga o endereço". Emint *p, q;, sópé ponteiro. - O tipo do ponteiro tem de combinar com o da variável; no gcc 15, misturar é erro.
- Ponteiro sem valor inicial guarda lixo: o compilador avisa, e usar pode dar
Falha de segmentação(139) ou, pior, estragar outra variável em silêncio. Próxima aula: Ponteiro Nulo.