Semana 11 — Ponteiros (parte 2) e passagem por referência¶
Nesta semana
Unidade 3 · Seg 19/10 · Qua 21/10 · Sex 23/10
Na Semana 10 você conheceu os ponteiros: variáveis que guardam endereços de
memória. Vimos o operador & (que devolve o endereço de uma variável) e o operador *
(que "olha para dentro" do endereço, a desreferência). Foi a base. Nesta semana
colocamos essa base para trabalhar de verdade — e resolvemos, finalmente, um mistério
que carregamos desde a Semana 01: por que o scanf sempre exigiu o &?
O clímax da semana é a passagem por referência. Você vai entender como, num mundo em que o C passa tudo por valor (Semana 04), conseguimos ainda assim escrever uma função que altera a variável de quem a chamou e que devolve vários resultados de uma vez. O truque — você já suspeita — são os ponteiros. Digite e execute cada exemplo: este é um tópico que só "cai a ficha" com as mãos no teclado.
🎯 Objetivos de aprendizagem¶
Ao final desta semana, você deverá ser capaz de:
- recapitular com segurança os operadores
&e*e a declaraçãoint *p(Semana 10); - explicar a relação entre ponteiros e vetores, incluindo a equivalência
v[i] == *(v + i), e por que o nome de um vetor "vale" o endereço do seu primeiro elemento; - explicar, finalmente, por que alterar um vetor dentro de uma função afeta o original (fechando a dúvida deixada na Semana 07);
- usar aritmética de ponteiros para percorrer um vetor, entendendo que
p + 1avança um elemento (deslocamento escalado pelosizeofdo tipo); - implementar a passagem por referência: passar
&xe receberint *p, alterando a variável original via*p— em contraste explícito com a passagem por valor da Semana 04; - escrever a função canônica
void troca(int *a, int *b)e funções que devolvem vários resultados por ponteiros (comodivide(..., int *q, int *r)); - explicar por que o
scanfusa&e usarconst int *ppara parâmetros de só leitura.
📖 Segunda — Estudo do conteúdo¶
1. Recapitulando a Semana 10¶
Antes de avançar, vamos firmar o terreno. Um ponteiro é uma variável cujo valor é um endereço de memória — ou seja, ele "aponta para" onde outra variável mora. Três peças formam o alfabeto dos ponteiros:
| Notação | Nome | O que faz |
|---|---|---|
int *p; |
declaração | cria um ponteiro p que aponta para um int |
&x |
operador endereço-de | devolve o endereço da variável x |
*p |
operador desreferência | acessa o conteúdo guardado no endereço que p aponta |
Um exemplo mínimo que junta os três:
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x; // p recebe o ENDERECO de x (p "aponta para" x)
printf("x = %d\n", x); // 42 (o valor de x)
printf("&x = %p\n", (void*) &x); // o endereco de x (algo como 0x7ffd...)
printf("p = %p\n", (void*) p); // o MESMO endereco (p guarda &x)
printf("*p = %d\n", *p); // 42 (*p desreferencia: o valor NO endereco)
*p = 100; // altera o CONTEUDO apontado por p...
printf("x = %d\n", x); // 100 (... ou seja, altera o proprio x!)
return 0;
}
A última parte é a semente de tudo que veremos hoje: escrever em *p é escrever em x,
porque p aponta para x. Guarde essa ideia — ela é o coração da passagem por referência.
A leitura correta de int *p
Leia int *p de trás para frente: "*p é um int". Ou seja, quando eu desreferenciar
p, obtenho um int — logo, p é um ponteiro para int. Essa leitura evita a confusão
entre o * da declaração (que diz "isto é um ponteiro") e o * da desreferência
(que diz "acesse o conteúdo").
Se algum desses pontos ainda parece nebuloso, releia a Semana 10 antes de continuar — o resto da semana depende dele.
2. Ponteiros e vetores: dois lados da mesma moeda¶
Aqui mora uma das ideias mais elegantes (e, no começo, mais confusas) do C. Considere:
Em C, o nome de um vetor, quando usado numa expressão, "vale" o endereço do seu primeiro elemento. Isto é:
Os 5 inteiros ficam lado a lado na memória (dizemos que o vetor é contíguo). Se
v[0] está no endereço, digamos, 1000, e cada int ocupa 4 bytes, então:
flowchart LR
subgraph mem["Memória (vetor contíguo)"]
direction LR
E0["v[0]<br/>10<br/>end. 1000"] --- E1["v[1]<br/>20<br/>end. 1004"] --- E2["v[2]<br/>30<br/>end. 1008"] --- E3["v[3]<br/>40<br/>end. 1012"] --- E4["v[4]<br/>50<br/>end. 1016"]
end
P["v (nome do vetor)<br/>vale 1000 = &v[0]"] --> E0
Como o nome v é um endereço, podemos aplicar a desreferência e a aritmética de
ponteiros (próxima seção) para chegar em qualquer elemento. A equivalência fundamental é:
Ou seja, a notação de colchetes v[i] é, literalmente, açúcar sintático para
*(v + i): "pegue o endereço do primeiro elemento, ande i posições, e desreferencie".
Vamos ver isso funcionando:
#include <stdio.h>
int main(void) {
int v[5] = {10, 20, 30, 40, 50};
printf("%d\n", v[2]); // 30 (notacao normal)
printf("%d\n", *(v + 2)); // 30 (a MESMA coisa, com ponteiros)
printf("%p\n", (void*) v); // endereco do primeiro elemento...
printf("%p\n", (void*) &v[0]); // ... identico a &v[0]
return 0;
}
Curiosidade 💡 — os colchetes são simétricos
Como v[i] é apenas *(v + i), e a soma é comutativa (v + i == i + v), o C aceita,
literalmente, escrever i[v] — e isso compila e funciona! 3[v] é o mesmo que v[3].
Ninguém escreve assim na vida real (é ilegível), mas o fato de ser válido revela que
os colchetes são, no fundo, pura aritmética de ponteiros disfarçada.
2.1 Fechando a dúvida da Semana 07¶
Na Semana 07 você aprendeu vetores e notou algo curioso: quando uma função altera um vetor, a mudança persiste fora dela. Isso contrariava diretamente a regra da Semana 04 ("função recebe cópia, não altera o original"). Ficou a pergunta no ar. Agora temos a resposta.
Quando você "passa um vetor" para uma função, o C não copia o vetor inteiro. Como o nome
do vetor vale o endereço do primeiro elemento, o que é copiado é apenas esse endereço.
A função recebe, portanto, um ponteiro para o mesmo vetor da main — e, ao mexer em
v[i], mexe no vetor original.
#include <stdio.h>
// Estes dois cabecalhos sao EQUIVALENTES em C:
// void dobra(int v[], int n)
// void dobra(int *v, int n) <- o C trata "int v[]" como "int *v"
void dobra(int v[], int n) {
for (int i = 0; i < n; i++) {
v[i] = v[i] * 2; // altera o vetor ORIGINAL (v aponta para ele)
}
}
int main(void) {
int nums[3] = {1, 2, 3};
dobra(nums, 3); // passa o ENDERECO do vetor (nao uma copia)
for (int i = 0; i < 3; i++)
printf("%d ", nums[i]); // 2 4 6 -> mudou de verdade!
printf("\n");
return 0;
}
Vetor não é exceção à passagem por valor
Muita gente memoriza "vetor passa por referência, escalar passa por valor". Isso é uma meia-verdade perigosa. O C sempre passa por valor. O que acontece é que, no caso do vetor, o valor copiado é um endereço. A cópia é do endereço, não dos dados — e é por isso que a função enxerga (e altera) o vetor original. Entender isto é entender ponteiros; decorar a "regra" é ficar preso na superfície.
3. Aritmética de ponteiros¶
Ponteiros suportam um tipo especial de aritmética. A regra de ouro:
p + 1não avança 1 byte — avança para o PRÓXIMO elemento do tipo apontado.
O compilador escala automaticamente o deslocamento pelo sizeof do tipo. Se p aponta
para um int (4 bytes) no endereço 1000, então p + 1 vale 1004, p + 2 vale 1008,
e assim por diante. Você raciocina em elementos; o C cuida dos bytes.
flowchart LR
subgraph m["Aritmética de ponteiros (int = 4 bytes)"]
direction LR
A["*p<br/>end. 1000"] --- B["*(p+1)<br/>end. 1004"] --- C["*(p+2)<br/>end. 1008"] --- D["*(p+3)<br/>end. 1012"]
end
Com isso, dá para percorrer um vetor sem índice nenhum, usando só o ponteiro:
#include <stdio.h>
int main(void) {
int v[5] = {10, 20, 30, 40, 50};
int *p = v; // p aponta para v[0] (lembre: v == &v[0])
// Forma 1: caminhando com aritmetica sobre p
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i)); // 10 20 30 40 50
}
printf("\n");
// Forma 2: incrementando o proprio ponteiro
for (p = v; p < v + 5; p++) { // p++ avanca um int por vez
printf("%d ", *p); // 10 20 30 40 50
}
printf("\n");
return 0;
}
Na Forma 2, p++ faz p "pular" para o próximo elemento, e v + 5 é o endereço logo
após o último — um limite natural para o laço.
Curiosidade 💡 — a aritmética escala pelo sizeof
Por que p + 1 avança 4 bytes para um int, mas avançaria 8 para um double e 1 para
um char? Porque o C multiplica o "1" pelo sizeof do tipo apontado. É como uma régua
cujas marcas têm o tamanho do elemento: para int, cada passo vale 4 bytes; para
double, 8. Isso é o que torna v[i] (isto é, *(v + i)) sempre correto,
independentemente do tipo — você nunca precisa multiplicar por sizeof na mão.
Não some ponteiros de tipos diferentes (nem invente endereços)
Aritmética de ponteiros só faz sentido dentro de um mesmo vetor (ou logo após o
último elemento, como limite). Acessar *(v + 5) num vetor de 5 posições, ou somar
ponteiros de tipos incompatíveis, é comportamento indefinido: pode "funcionar" hoje
e corromper a memória amanhã. A régua só é confiável dentro do vetor.
4. Passagem por referência — o clímax da semana¶
Reencontremos a limitação da Semana 04. Lá vimos que o C passa argumentos por valor: a função recebe uma cópia, e mexer nela não afeta a original. O exemplo clássico:
void tenta_dobrar(int x) {
x = x * 2; // altera apenas a COPIA local
}
// na main: n = 10; tenta_dobrar(n); -> n continua 10
A pergunta que ficou em aberto (na caixa !!! question da Semana 04) foi: "e se eu
precisar que a função altere a variável original?". A resposta é a passagem por
referência, e ela tem três passos:
- Em vez do valor da variável, passe o seu endereço:
funcao(&x). - A função recebe esse endereço num parâmetro ponteiro:
void funcao(int *p). - Dentro da função, use a desreferência para alcançar e alterar a original:
*p = ....
Assim, a função não recebe uma cópia do valor de x — recebe uma cópia do endereço
de x. E, com o endereço em mãos, ela chega até a caixa original e escreve nela.
#include <stdio.h>
void dobra_de_verdade(int *p) {
*p = *p * 2; // escreve no CONTEUDO apontado -> altera a original
}
int main(void) {
int n = 10;
dobra_de_verdade(&n); // passa o ENDERECO de n
printf("%d\n", n); // 20 -> mudou!
return 0;
}
O contraste com a Semana 04 é total. Vale colocar lado a lado:
| Passagem por valor (Sem. 04) | Passagem por referência (Sem. 11) | |
|---|---|---|
| Parâmetro | int x |
int *p |
| Chamada | f(n) |
f(&n) |
| O que é copiado | o valor de n |
o endereço de n |
| Dentro da função | mexe na cópia | mexe no original (via *p) |
| Original muda? | não | sim |
Erro clássico: querer alterar sem usar ponteiro
Escrever void dobra(int x){ x = x*2; } e esperar que a variável de fora mude é o erro
número um deste tópico. Compila sem reclamar, mas não faz nada de útil para o
chamador. Se a função precisa alterar o original, o parâmetro tem que ser ponteiro
(int *) e a chamada tem que passar o endereço (&). Não há meio-termo.
Curiosidade 💡 — por que o scanf sempre exigiu o &
Este é o mistério da Semana 01, finalmente resolvido! O scanf precisa guardar o
valor lido na sua variável — ou seja, precisa alterar uma variável do chamador.
Isso é exatamente passagem por referência. Por isso você escreve scanf("%d", &idade):
você entrega ao scanf o endereço de idade, e ele, lá dentro, faz o equivalente a
*p = valor_lido;. O printf, ao contrário, só lê os valores (não precisa mudar
nada), e por isso não usa &: printf("%d", idade). A regra que você decorou há dez
semanas era, o tempo todo, um caso de ponteiros.
5. O exemplo canônico: void troca(int *a, int *b)¶
Trocar o conteúdo de duas variáveis é impossível com passagem por valor (a função só mexeria em cópias). Com ponteiros, é direto — e este é o "Olá, mundo" da passagem por referência:
#include <stdio.h>
void troca(int *a, int *b) {
int temp = *a; // guarda o conteudo apontado por a
*a = *b; // no lugar de *a, poe o conteudo de *b
*b = temp; // no lugar de *b, poe o valor guardado
}
int main(void) {
int x = 5, y = 9;
printf("antes: x = %d, y = %d\n", x, y); // x = 5, y = 9
troca(&x, &y); // passa os ENDERECOS
printf("depois: x = %d, y = %d\n", x, y); // x = 9, y = 5
return 0;
}
Vale seguir o passo a passo com x = 5 e y = 9. Na chamada troca(&x, &y), o
parâmetro a passa a apontar para x, e b para y:
flowchart LR
subgraph main["main"]
X["x = 5"]
Y["y = 9"]
end
subgraph troca["troca"]
A["a →"] --> X
B["b →"] --> Y
end
int temp = *a;→temprecebe*a, ou seja5.*a = *b;→ o conteúdo apontado pora(isto é,x) vira*b, ou seja9. Agorax = 9.*b = temp;→ o conteúdo apontado porb(isto é,y) viratemp, ou seja5. Agoray = 5.
Ao voltar para a main, x e y estão trocados de verdade, porque troca operou sobre
os endereços reais das duas variáveis.
A versão errada que parece certa
Compare com esta versão quebrada:
void troca_errada(int a, int b) { // por VALOR: recebe copias
int temp = a; a = b; b = temp; // troca as copias locais...
} // ... e as joga fora ao retornar
troca_errada(x, y), nada muda em x e y — a função trocou duas cópias que
morrem no fim. É um ótimo experimento para a sexta-feira: rode as duas e compare.
6. Devolvendo vários resultados por ponteiros¶
O return devolve um só valor. Mas e quando o problema pede dois ou mais resultados
de uma vez? A passagem por referência resolve: cada resultado sai por um ponteiro-parâmetro.
O exemplo perfeito é a divisão inteira, que produz quociente e resto ao mesmo tempo:
#include <stdio.h>
// Nao usa return: entrega os resultados escrevendo em *quociente e *resto.
void divide(int a, int b, int *quociente, int *resto) {
*quociente = a / b; // parte inteira da divisao
*resto = a % b; // resto da divisao
}
int main(void) {
int q, r;
divide(17, 5, &q, &r); // passa &q e &r para a funcao preencher
printf("17 / 5 = %d, resto %d\n", q, r); // 3, resto 2
return 0;
}
Repare no padrão: a e b entram por valor (são só dados de entrada), enquanto q e r
entram por referência (são saídas que a função deve preencher). Misturar os dois estilos
na mesma função é comum e perfeitamente idiomático em C.
Entrada por valor, saída por ponteiro
Uma convenção útil ao ler a assinatura de uma função C: parâmetros "normais" (int a)
costumam ser entradas; parâmetros ponteiros (int *r) costumam ser saídas (ou
dados grandes que não vale a pena copiar, como vetores). Isso é só uma convenção, não uma
regra da linguagem — mas ajuda muito a entender funções da biblioteca padrão.
7. Protegendo o que é apontado: const¶
Se um parâmetro ponteiro serve só para leitura (a função não deve alterar o que ele
aponta), é boa prática declará-lo com const. Isso documenta a intenção e faz o
compilador impedir uma escrita acidental:
#include <stdio.h>
// "const int *v": a funcao promete NAO alterar o conteudo de v.
int soma_vetor(const int *v, int n) {
int soma = 0;
for (int i = 0; i < n; i++) {
soma += v[i]; // leitura: OK
// v[i] = 0; // se descomentar, o compilador ACUSA ERRO (bom!)
}
return soma;
}
int main(void) {
int nums[4] = {1, 2, 3, 4};
printf("%d\n", soma_vetor(nums, 4)); // 10
return 0;
}
Você verá const int * o tempo todo na biblioteca padrão — por exemplo, funções que recebem
uma string apenas para lê-la a declaram como const char *. É um contrato: "pode me
entregar seus dados; prometo que não vou mexer neles".
Curiosidade 💡 — ponteiros para ponteiros (int **)
Se um ponteiro pode apontar para um int, nada impede que um ponteiro aponte para
outro ponteiro. Um int **pp é um "ponteiro para ponteiro para int". Parece
abstrato, mas é exatamente o que permite uma função alterar um ponteiro de quem a
chamou (a mesma ideia de troca, um nível acima) — e é a base das matrizes e dos
vetores de strings (char **argv, que você já viu de relance na main). Vamos abrir
essa caixa na Semana 13. Por ora, saiba que a ideia
se repete: acrescentar um * sobe um nível de indireção.
8. Erros comuns desta semana (resumão)¶
Checklist de armadilhas
- Querer alterar a variável do chamador com passagem por valor (
int x) em vez de ponteiro (int *p). - Esquecer o
&na chamada:troca(x, y)em vez detroca(&x, &y). - Confundir o
*da declaração (int *p) com o*da desreferência (*p = 5). - Achar que
p + 1avança 1 byte (avança 1 elemento, escalado pelosizeof). - Acessar além do vetor com aritmética (
*(v + n)num vetor denposições). - Desreferenciar um ponteiro não inicializado (aponta para "lixo") — trava certa.
- Esquecer que
int v[]como parâmetro éint *v(não se copia o vetor). - Tentar escrever em um
const int *(o compilador acusa — e isso é bom).
👥 Quarta — Encontro¶
Chegue ao encontro tendo estudado o material acima e tendo executado os exemplos —
especialmente o troca.c. O encontro é para consolidar e destravar dúvidas, não para ver o
conteúdo pela primeira vez. Roteiro previsto:
- Revisão relâmpago:
&e*, a equivalênciav[i] == *(v + i), e o mantra "para alterar o original, passe o endereço". - Visualização passo a passo da passagem por referência, desenhando as "caixas" da memória e as setas dos ponteiros (retomando a ideia da pilha de chamadas da Semana 04).
- Galeria de erros: provocamos de propósito o erro de esquecer o
&e o de usarint xno lugar deint *p, para você aprender a reconhecer o sintoma (a variável "não muda").
Problema-guia do encontro — normalizar um intervalo: ler dois inteiros a e b e
garantir que fiquem em ordem crescente (a <= b); se estiverem trocados, corrigir no
lugar, usando a função troca. Depois, ler um vetor e imprimir, de uma só chamada, o
maior e o menor — via ponteiros.
#include <stdio.h>
void troca(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
// Devolve DOIS resultados (maior e menor) por referencia.
void maior_e_menor(const int *v, int n, int *maior, int *menor) {
*maior = v[0];
*menor = v[0];
for (int i = 1; i < n; i++) {
if (v[i] > *maior) *maior = v[i];
if (v[i] < *menor) *menor = v[i];
}
}
int main(void) {
int a = 9, b = 5;
if (a > b) troca(&a, &b); // garante a <= b, corrigindo no lugar
printf("ordenados: a = %d, b = %d\n", a, b); // a = 5, b = 9
int v[6] = {7, 2, 9, 4, 9, 1};
int mx, mn;
maior_e_menor(v, 6, &mx, &mn); // uma chamada, dois resultados
printf("maior = %d, menor = %d\n", mx, mn); // maior = 9, menor = 1
return 0;
}
Perguntas para pensar antes do encontro: por que maior_e_menor usa const em v, mas
não em maior e menor? O que aconteceria se trocássemos troca(&a, &b) por
troca(a, b)?
✍️ Sexta — Estudo dirigido¶
Reserve este tempo para praticar de verdade. Diferentemente das semanas de laços e condicionais, a passagem por referência é pouco testada isoladamente no Beecrowd — ela costuma aparecer "embutida" em problemas maiores. Por isso, mais do que resolver problemas novos, o objetivo desta sexta é revisitar exercícios que você já fez e refatorá-los para usar ponteiros e funções que devolvem resultados por referência.
Exercícios guiados (Beecrowd)¶
| Nº | Problema | Como aplicar ponteiros/referência |
|---|---|---|
| 1116 | Dividindo Valores | escreva void divide(int a, int b, int *q, int *r) |
| 1187 | Faixa de Números | preencha um vetor via função (parâmetro int v[]) |
| 1173 | Preenchimento de Vetor I | função que preenche o vetor do chamador |
| 1478 | Máximo e Mínimo | devolva maior e menor por dois ponteiros |
(Poucos e propositalmente familiares: a meta é reescrever com o novo ferramental, não vencer a dificuldade do enunciado.)
Desafios para escrever do zero¶
void troca(int *a, int *b)— implemente e teste com vários pares. Depois escreva a versão erradatroca(int a, int b)e comprove que ela não mudaxey.void maior_e_menor(const int *v, int n, int *maior, int *menor)— devolve os dois extremos de um vetor numa só chamada.void divide(int a, int b, int *quociente, int *resto)— quociente e resto de uma só vez. Teste com17 / 5,20 / 4,7 / 10.- Refatoração por referência: pegue uma função sua que hoje usa
return(por exemplo,double area_retangulo(...)) e reescreva-a comovoid area_retangulo(double b, double h, double *area), "devolvendo" o resultado pelo ponteiro. Compare os dois estilos: qual fica mais legível? (Dica: para um resultado,returncostuma ganhar; ponteiros brilham quando são vários.) - Percurso com ponteiro: escreva uma função que imprime um vetor sem usar índice —
apenas com um ponteiro
pe aritmética (for (p = v; p < v + n; p++)).
Para investigar (curiosidade prática)¶
- Imprima e compare os quatro valores a seguir para um vetor
int v[3]:v,&v[0],v + 1,&v[1]. O que você conclui sobrevversus&v[0]? E sobre a diferença, em bytes, entrevev + 1? (Use%pe o cast(void*).) - Confirme na prática que
troca(&x, &y)mudaxey, enquanto a versãotroca(int a, int b)não muda. Explique, com suas palavras, por quê — usando as palavras "cópia", "endereço" e "desreferência".
✅ Checklist de autoavaliação¶
Marque com sinceridade — se algum item não estiver ✅, volte à seção correspondente.
- Explico os operadores
&e*e sei lerint *pcomo "*pé umint". - Entendo que o nome de um vetor vale
&v[0]e quev[i] == *(v + i). - Sei explicar por que uma função altera o vetor original (recebe um ponteiro).
- Sei que
p + 1avança um elemento (escalado pelosizeof) e percorro um vetor com aritmética de ponteiros. - Implemento passagem por referência:
f(&x)na chamada,int *pno parâmetro,*p = ...para alterar o original. - Escrevo
void troca(int *a, int *b)de cabeça e sigo seu passo a passo. - Sei devolver vários resultados por ponteiros (
divide(..., int *q, int *r)). - Explico por que o scanf usa
&e usoconst int *para parâmetros de só leitura.
🔗 Referências e para se aprofundar¶
- Material 2026.1 — Aula 11: Ponteiros e Alocação Dinâmica.
- BACKES, André. Linguagem C: completa e descomplicada (ponteiros, ponteiros e vetores, passagem de parâmetros por referência).
- KERNIGHAN, B.; RITCHIE, D. The C Programming Language (o clássico "K&R"), cap. 5 (Pointers and Arrays) — a referência definitiva sobre a relação ponteiro/vetor.
- Referência: cppreference — pointer (detalhes de aritmética de ponteiros e conversões).
- Prática: Beecrowd.
Antes de seguir para a Semana 12
Você acaba de dominar a peça que faltava: com ponteiros, uma função pode alterar o
mundo à sua volta, não só devolver um número. Mas todos os endereços que manipulamos até
aqui pertenciam a variáveis que o compilador criou para nós — na pilha, com tamanho
fixo, decidido em tempo de compilação. E se precisarmos de um vetor cujo tamanho só
conhecemos durante a execução? Para isso, teremos que pedir memória à máquina nós
mesmos — a alocação dinâmica (malloc, free), o tema da
Semana 12. É onde os ponteiros deixam de ser um recurso a
mais e passam a ser indispensáveis.