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.

Você vai aprender
  • 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óriaEm C
A gaveta com o dinheiroUma variável comum: int economias = 50;
O bilhete com "gaveta 1000"Um ponteiro: int *p = &economias;
Ir até a gaveta escrita no bilhete e olharLer pelo ponteiro: *p
Ir até a gaveta e trocar o conteúdoEscrever pelo ponteiro: *p = 80;
Escrever outro número no bilheteApontar 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 o int, forma o tipo int *, que se lê "ponteiro para int".
p
O nome do ponteiro. p é comum em exemplos curtos; em programas de verdade, prefira nomes que digam para onde ele aponta, como p_pontos ou atual.
= &pontos
O valor inicial: o endereço de pontos. A partir daqui, dizemos que "p aponta para pontos".

E nas linhas do printf:

  • p sozinho é o conteúdo do bilhete: o endereço guardado. Para mostrar, %p com (void *), como qualquer endereço.
  • *p é "vá até o endereço que está em p e pegue o que tem lá". Isso se chama desreferenciar o ponteiro (seguir a referência). Como lá está um int, o formato é %d.
  • &p é o endereço do próprio ponteiro. O bilhete também está guardado em algum lugar.
…7ec10…7f00x…7ecpontos (int)p (int *)aponta para
As gavetas da execução acima. Dentro de 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çoNomeTipo (tamanho)O que está guardado
0x7ffdcfd4b7ecpontosint (4 bytes)10
0x7ffdcfd4b7f0pint * (8 bytes)0x7ffdcfd4b7ec
Como ler em voz alta 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
LinhaO que acontecepontos
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
710&biaanabiapagora aponta para bia
Depois de 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ê escreveMuda o quêNa história
p = &bia;O ponteiro (para onde ele aponta)Reescreve o número no bilhete
*p = 10;A variável apontadaVai 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 apareceExemploQuer 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 valoresa * bMultiplicaçã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.

Erro, não aviso, no gcc 15 Até o gcc 13, misturar tipos de ponteiro (e guardar um número comum num ponteiro) dava só um aviso, e o programa era gerado mesmo assim. Desde o gcc 14, é erro. O Clang do editor também recusa: 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.

O perigo de verdade é quando NÃO dá erro Aqui o programa morreu, e isso foi sorte: você ficou sabendo do problema. Se o lixo fosse, por acaso, o endereço de uma gaveta do próprio programa, o *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

FormaComentárioUse?
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

"makes pointer from integer without a cast": esqueceu o &
#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 &").

"makes integer from pointer without a cast": * 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.

"incompatible pointer type": tipos diferentes
$ 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;.

"‘p’ is used uninitialized" e depois "Falha de segmentação"
$ 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ódigoO 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.
*pA 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.
&pO 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:

= &altura;

2. Desreferenciar

O ponteiro p aponta para vidas. Complete para guardar 3 em vidas usando o ponteiro:

= 3;

3. Apontar

Complete para fazer o ponteiro p apontar para a variável nota:

p = ;

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 para int que aponta para x.
  • *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". Em int *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.