Semana 10 — Estrutura de memória e ponteiros (parte 1)¶
Nesta semana
Unidade 3 · Seg 12/10 · Qua 14/10 · Sex 16/10 · ⚠️ feriado: 12/10 (N. Sra. Aparecida)
Chegamos ao assunto mais desafiador — e talvez o mais poderoso — de toda a disciplina: memória e ponteiros. Se em algum momento do curso você vai precisar ir devagar, respirar fundo e reler com calma, é agora. A boa notícia: nada aqui é "mágica". É tudo consequência lógica de uma ideia simples que você já conhece desde a Semana 01 — a de que variáveis vivem na memória. Vamos, finalmente, abrir essa caixa e olhar dentro dela.
Como a segunda-feira (12/10) é feriado, use esse dia para uma primeira leitura tranquila. Não tenha pressa: o investimento que você fizer nesta semana e na próxima (ponteiros parte 2) rende juros no resto do curso — e no resto da sua vida como programador(a). Muita gente "trava" em ponteiros por pular etapas. Você não vai pular: vamos construir tudo do zero, com desenhos e analogias.
🎯 Objetivos de aprendizagem¶
Ao final desta semana, você deverá ser capaz de:
- explicar como a memória de um programa é organizada em bytes endereçados e conhecer, em alto nível, as regiões código, globais/estáticas, heap e pilha (stack);
- usar o operador
&("endereço de") e imprimir endereços com%p— e entender, enfim, por que oscanfsempre pediu&; - explicar o que é um ponteiro (uma variável que guarda um endereço) e declarar
ponteiros com a sintaxe
tipo *p; - atribuir o endereço de uma variável a um ponteiro (
int *p = &x;); - usar o operador de desreferência
*para ler e alterar o valor apontado, e distinguir o*da declaração do*da desreferência; - entender o ponteiro nulo
NULL, por que sempre inicializar ponteiros e por que nunca desreferenciarNULL(o famoso segmentation fault); - ler e prever, sem erro, expressões que misturam
&x,p,*pe&p.
📖 Segunda — Estudo do conteúdo¶
1. Retomando: variáveis vivem na memória¶
Lá na Semana 01 usamos a analogia da caixa etiquetada:
uma variável é um espaço nomeado na memória RAM que guarda um valor. A etiqueta é o
nome (idade), o conteúdo é o valor (20) e o tamanho/formato da caixa vem do tipo
(int).
Dissemos, também, que quando o programa roda ele é carregado do disco para a RAM, e as variáveis passam a existir ali durante a execução. Só ficou faltando uma pergunta: onde, exatamente, na memória? É essa pergunta que abre a Unidade 3 — e a resposta é a chave de tudo o que vem a seguir. Guarde a analogia da caixa: vamos agora descobrir que cada caixa tem também um número de casa (um endereço).
2. Como a memória é organizada: bytes numerados¶
A memória RAM pode ser imaginada como um prédio enorme de caixas de correio, todas iguais, enfileiradas. Cada caixinha guarda 1 byte (8 bits) e tem um número único — o seu endereço. Os endereços são inteiros que começam em 0 e vão crescendo, um por byte, até o fim da memória.
flowchart LR
subgraph mem["Memória: bytes numerados sequencialmente"]
direction LR
B0["endereco 1000<br/>1 byte"] --- B1["1001<br/>1 byte"] --- B2["1002<br/>1 byte"] --- B3["1003<br/>1 byte"] --- B4["1004<br/>1 byte"]
end
Uma variável int idade = 20; não ocupa só 1 byte: um int ocupa 4 bytes (veja a
tabela de tipos da Semana 01). Então idade ocupa 4 caixinhas consecutivas — por
exemplo, os endereços 1000, 1001, 1002 e 1003. O endereço da variável é, por convenção,
o primeiro desses bytes (o 1000). O tipo (int) é o que informa ao computador
"leia 4 bytes a partir daqui e interprete como um número inteiro".
Curiosidade 💡 — cada byte tem endereço, mas lemos em blocos
A memória é endereçada por byte: todo byte, individualmente, tem um número. Mas
quase nunca trabalhamos com 1 byte isolado — trabalhamos com variáveis, que ocupam
vários bytes conforme o tipo. É o tipo que diz quantos bytes formam o valor e
como interpretá-los. Por isso o mesmo endereço "1000" pode ser o começo de um int
(4 bytes) ou de um char (1 byte): o endereço aponta o começo; o tipo diz o
tamanho.
2.1 As regiões de memória de um programa¶
Quando o sistema operacional coloca seu programa para rodar, ele reserva um espaço de memória e o organiza em regiões com finalidades diferentes. Não precisamos decorar tudo, mas conhecer o mapa ajuda muito a entender ponteiros:
| Região | O que guarda | Tempo de vida |
|---|---|---|
| Código (text) | As instruções do programa (o executável em si). | Toda a execução (só leitura). |
| Globais / estáticas | Variáveis globais e static (Semana 04). |
Toda a execução. |
| Heap | Memória pedida manualmente pelo programa (Semana 11: malloc). |
Até você liberar (free). |
| Pilha (stack) | Variáveis locais e parâmetros de cada função. | Nasce e morre com a chamada da função. |
flowchart TB
subgraph prog["Memoria de um programa em execucao"]
direction TB
S["PILHA (stack)<br/>variaveis locais, parametros<br/>cresce para baixo ⬇"]
G1[" "]
H["HEAP<br/>memoria dinamica (malloc)<br/>cresce para cima ⬆"]
D["GLOBAIS / ESTATICAS<br/>globais e static"]
T["CODIGO (text)<br/>as instrucoes do programa"]
S --- G1 --- H --- D --- T
end
A pilha e o heap crescem "um em direção ao outro" a partir das pontas opostas do espaço livre — por isso o desenho os coloca nas extremidades.
2.2 A pilha: reencontrando a "pilha de chamadas" da Semana 04¶
Você já conhece a pilha! Na Semana 04, na curiosidade sobre a pilha de chamadas (call stack), dissemos: "a cada chamada de função, um quadro (stack frame) é empilhado com os parâmetros e as variáveis locais daquela chamada; quando a função retorna, o quadro é desempilhado". É exatamente esta região.
É por isso que uma variável local "morre" quando a função termina: o quadro dela é desempilhado, e aquele espaço fica livre para a próxima chamada. E é por isso que cada chamada tem suas próprias cópias das locais. A stack é a memória rápida e automática; o heap (Semana 11) é a memória que você controla à mão.
Por que estudar o mapa da memória?
Ponteiros são, no fundo, endereços dessas regiões. Muitos bugs difíceis (e o temido segmentation fault) acontecem quando um ponteiro aponta para um lugar errado ou inválido da memória. Entender o mapa é entender por que o programa quebra — e como consertar.
3. O operador &: o "endereço de"¶
Como descobrir onde uma variável mora? Com o operador & ("endereço de"),
colocado antes do nome da variável. &idade significa, literalmente,
"o endereço da variável idade".
E aqui vem uma revelação: é o mesmíssimo & do scanf! Desde a Semana 01 você escreve
scanf("%d", &idade); — e a gente pediu para você aceitar o & "como uma regra, por
enquanto". Chegou a hora de entender. O scanf precisa saber em que endereço
depositar o valor lido; &idade entrega esse endereço. Simples assim.
Podemos imprimir um endereço com o especificador %p (de pointer):
#include <stdio.h>
int main(void) {
int idade = 20;
double altura = 1.75;
printf("valor de idade: %d\n", idade);
printf("endereco de idade: %p\n", (void *) &idade); // %p com cast para (void *)
printf("endereco de altura: %p\n", (void *) &altura);
return 0;
}
Uma saída possível (os números mudam a cada execução!):
Os endereços aparecem em hexadecimal (começando com 0x) — é só a forma tradicional de
escrever esses números grandes. Não decore o valor: o que importa é entender que
&idade é "o número da caixa de correio" onde idade está guardada.
Curiosidade 💡 — por que o scanf sempre pediu &? (agora faz sentido)
Uma função em C recebe os argumentos por valor (Semana 04): ela ganha uma cópia.
Se o scanf recebesse apenas a cópia do valor de idade, ele não teria como
alterar a idade original de quem chamou. A solução? Passar o endereço — assim o
scanf vai diretamente naquela caixa da memória e deposita ali o número digitado.
O & do scanf é, portanto, o seu primeiro contato com passagem por referência —
tema central da Semana 11. Você usava ponteiros sem saber!
O (void *) no %p
Para imprimir um endereço com %p de forma correta e portável, converta-o para
(void *): printf("%p", (void *) &idade);. É um detalhe técnico; se você esquecer, o
compilador com -Wall costuma avisar. Aceite o padrão por ora.
4. O que é um ponteiro¶
Se um endereço é apenas um número (o número da caixa), nada nos impede de guardar esse número em uma variável. Uma variável que guarda o endereço de outra variável é o que chamamos de ponteiro.
Um ponteiro é uma variável cujo conteúdo é um endereço de memória.
Pense assim: se idade é uma casa, um ponteiro é um papelzinho onde você anotou o
endereço da casa. O papel não é a casa — é só a informação de onde ela fica. E,
tendo o endereço, você consegue chegar até a casa e mexer no que há dentro.
4.1 Declarando um ponteiro¶
Declaramos um ponteiro colocando um asterisco * entre o tipo e o nome:
int *p; // p e um ponteiro para int (guarda o endereco de um int)
double *q; // q e um ponteiro para double
char *c; // c e um ponteiro para char
Leia int *p; como: "p é um ponteiro para int" — ou seja, p vai guardar o
endereço de alguma variável do tipo int. O tipo apontado (int) importa: ele diz ao
ponteiro quantos bytes ler e como interpretá-los quando formos até o endereço.
flowchart LR
P["p (ponteiro)<br/>┌──────────┐<br/>│ 1000 │<br/>└──────────┘<br/>guarda o endereco"] -->|aponta para| X["x (int)<br/>endereco 1000<br/>┌────┐<br/>│ 10 │<br/>└────┘<br/>guarda o valor"]
Onde colocar o *? int *p, int* p ou int * p?
As três formas compilam e são equivalentes. Mas há uma pegadinha ao declarar
vários ponteiros na mesma linha: o * vale apenas para o nome logo à sua direita.
int* a, b; // CUIDADO: 'a' e ponteiro, mas 'b' e um int comum!
int *a, *b; // certo: os dois sao ponteiros (o * antes de cada nome)
* colado no nome (int *p): deixa claro
que o * "pertence" àquela variável específica.
5. Atribuindo um endereço a um ponteiro¶
Um ponteiro recém-declarado ainda não aponta para nada útil. Para fazê-lo apontar para
uma variável, atribuímos a ela o endereço dessa variável (com o &):
int x = 10; // x e um int comum, com valor 10
int *p = &x; // p passa a guardar o ENDERECO de x -> "p aponta para x"
Repare na coerência dos tipos: x é int, então &x é "endereço de int", e p é
declarado como int * ("ponteiro para int"). Tudo encaixa. Se você tentasse
double *q = &x; (ponteiro para double recebendo endereço de int), o compilador reclamaria.
flowchart LR
subgraph antes["Depois de: int x = 10; int *p = &x;"]
direction LR
PP["p<br/>┌──────┐<br/>│ &x │<br/>└──────┘"] -->|aponta para| XX["x<br/>┌────┐<br/>│ 10 │<br/>└────┘"]
end
6. O operador *: desreferência ("conteúdo de")¶
Ter o endereço é ótimo, mas o que a gente quer de verdade é chegar até a variável e
usar o valor que está lá. Para isso existe o operador de desreferência, o
asterisco * colocado antes de um ponteiro. *p significa
"o conteúdo do endereço guardado em p" — ou seja, o valor da variável apontada.
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x; // p aponta para x
printf("%d\n", x); // 10 (o valor de x, direto)
printf("%d\n", *p); // 10 (o valor APONTADO por p -> tambem x)
*p = 20; // altera o CONTEUDO apontado por p... que e o x!
printf("%d\n", x); // 20 (x mudou, mesmo sem mencionar x!)
return 0;
}
A linha *p = 20; é o coração do capítulo. Ela não muda p (o endereço continua o
mesmo); ela vai até o endereço guardado em p e escreve 20 lá dentro. Como esse
endereço é o de x, quem muda é o próprio x. Foi assim que o scanf sempre mexeu nas
suas variáveis!
6.1 O * da declaração ≠ o * da desreferência¶
Este é, disparado, o ponto que mais confunde. O mesmo símbolo * faz coisas diferentes
dependendo de onde aparece:
| Onde aparece | O que o * significa |
Exemplo |
|---|---|---|
| Na declaração | "esta variável é um ponteiro" | int *p; |
| Em uma expressão (uso) | "desreferenciar: o valor apontado" | *p = 20; |
int *p = &x; // AQUI o * faz parte da DECLARACAO: "p e um ponteiro"
*p = 20; // AQUI o * e DESREFERENCIA: "escreva 20 no que p aponta"
Uma dica para não se perder: se há um tipo logo antes (int *p), o * é de
declaração; se o * aparece "solto" numa expressão (*p, y = *p), é
desreferência.
Erro clássico: confundir p com *p
pé o endereço (para onde o ponteiro aponta).*pé o valor que está naquele endereço.
Escrever p = 20; faz p "apontar para o endereço 20" (quase sempre um lugar
inválido — desastre à vista). O que você queria era *p = 20; (escrever 20 no
destino). Leia sempre *p como "o conteúdo de".
7. O ponteiro nulo NULL¶
E se um ponteiro ainda não aponta para nada? Assim como uma variável int não
inicializada contém lixo (Semana 01), um ponteiro não inicializado contém um endereço
qualquer, imprevisível — e usá-lo é perigosíssimo. A convenção da linguagem é inicializar
esses ponteiros com NULL, um valor especial que significa "não aponto para lugar
nenhum".
#include <stdio.h> // NULL vem definido aqui (e em outros cabecalhos)
int *p = NULL; // p nao aponta para nada, de forma explicita e segura
if (p != NULL) {
printf("%d\n", *p); // so desreferencia se houver algo valido
} else {
printf("ponteiro nulo: nao ha para onde ir.\n");
}
Duas regras de ouro:
- Sempre inicialize um ponteiro — com um endereço válido (
&x) ou comNULL. - Nunca desreferencie
NULL(nem um ponteiro inválido).*pquandopéNULLquebra o programa.
Curiosidade 💡 — NULL e o famoso segmentation fault
Desreferenciar um ponteiro nulo ou inválido (*p com p == NULL, por exemplo) é uma
das causas número um de travamentos em C. O sistema operacional detecta a tentativa de
acessar uma região de memória proibida e mata o programa na hora, com a mensagem
Segmentation fault (ou "falha de segmentação"). Se você vir isso, a primeira
suspeita deve ser: "estou usando um ponteiro que não aponta para um lugar válido?".
NULL existe justamente para você testar antes (if (p != NULL)) e evitar o
desastre.
8. Quadro-resumo: &x, p, *p, &p¶
Junte tudo. Considere:
| Escrevo... | Leio como... | Vale... |
|---|---|---|
x |
"o valor de x" | 10 |
&x |
"o endereço de x" | ex.: 0x7ffc...c5c |
p |
"o endereço guardado em p" | o mesmo de &x (p aponta para x) |
*p |
"o valor apontado por p" (conteúdo) | 10 (é o próprio x) |
&p |
"o endereço do próprio ponteiro p" | outro endereço (onde p mora) |
Sim: o ponteiro também é uma variável e, portanto, também tem o seu próprio endereço
(&p). Um ponteiro que guarda o endereço de outro ponteiro existe (é o int **) e será
útil mais adiante — por ora, basta saber que &p faz sentido.
flowchart LR
subgraph vis["&x, p, *p, &p"]
direction LR
PBOX["p<br/>(mora em &p)<br/>┌──────┐<br/>│ &x │<br/>└──────┘"] -->|p aponta| XBOX["x<br/>(mora em &x)<br/>┌────┐<br/>│ 10 │ <- *p<br/>└────┘"]
end
Curiosidade 💡 — todo ponteiro tem o mesmo tamanho
Um int ocupa 4 bytes, um double ocupa 8, um char ocupa 1... e um ponteiro?
Todos os ponteiros têm o mesmo tamanho, não importa o tipo apontado — porque todos
guardam a mesma coisa: um endereço. Em um sistema 64 bits, um endereço ocupa
8 bytes; em sistemas de 32 bits, 4 bytes. Então sizeof(int *), sizeof(double *)
e sizeof(char *) dão o mesmo resultado (tipicamente 8 na sua máquina). Faz
sentido: o "papelzinho com o endereço" tem o mesmo tamanho, quer a casa seja um
barraco ou um palácio.
9. Erros comuns desta semana (resumão)¶
Checklist de armadilhas
- Confundir
p(o endereço) com*p(o valor apontado). - Confundir o
*da declaração (int *p) com o*da desreferência (*p = ...). - Declarar
int* a, b;achando quebtambém é ponteiro (não é!). - Usar um ponteiro não inicializado (aponta para lixo) — inicialize com
&xouNULL. - Desreferenciar
NULLou ponteiro inválido → Segmentation fault. - Esquecer o
&ao guardar o endereço de uma variável (p = x;em vez dep = &x;). - Tipos incompatíveis (
double *q = &x;comxsendoint). - Imprimir endereço sem
%p(ou sem o(void *)).
👥 Quarta — Encontro¶
Chegue ao encontro tendo estudado o material acima e tendo executado os exemplos — este é o assunto em que "ver de longe" menos funciona. Você precisa ter desenhado as caixas e as setas. Roteiro previsto:
- Revisão relâmpago: memória como bytes endereçados; regiões (stack × heap); o
&e o*; ponteiro como "variável que guarda endereço". - Desenho ao vivo: vamos executar um programa passo a passo, desenhando na tela cada variável, seu endereço e o que cada ponteiro aponta — o "modelo de caixas e setas".
- Galeria de erros: provocamos de propósito o
*pnum ponteiroNULL, a confusãop×*pe oint* a, b;para você aprender a reconhecer os sintomas (inclusive o segmentation fault).
Problema-guia do encontro — "a espiã da memória": um programa que declara algumas variáveis, cria ponteiros para elas, imprime endereços e valores, e depois altera as variáveis através dos ponteiros, confirmando as mudanças. É o "olá, mundo" dos ponteiros.
#include <stdio.h>
int main(void) {
int a = 5;
int b = 42;
int *p = &a; // p aponta para a
int *q = &b; // q aponta para b
// 1) Onde cada variavel mora e o que guarda:
printf("a = %d (mora em %p)\n", a, (void *) &a);
printf("b = %d (mora em %p)\n", b, (void *) &b);
// 2) O que os ponteiros guardam e o que apontam:
printf("p guarda %p -> *p = %d\n", (void *) p, *p);
printf("q guarda %p -> *q = %d\n", (void *) q, *q);
// 3) Alterando 'a' SEM mencionar 'a', so pelo ponteiro:
*p = 100;
printf("depois de *p = 100: a = %d\n", a); // a virou 100
// 4) Fazendo p apontar para b (mudamos o ENDERECO em p, nao o valor):
p = &b;
printf("agora *p = %d (p aponta para b)\n", *p); // 42
return 0;
}
Perguntas para pensar antes do encontro: por que a mudou na etapa 3 sem escrevermos
a = ...? Na etapa 4, o que muda quando fazemos p = &b — o valor de b ou o endereço
guardado em p? Qual seria a diferença se tivéssemos escrito *p = b;?
✍️ Sexta — Estudo dirigido¶
Reserve este tempo para desenhar e executar. Em ponteiros, a técnica que mais ajuda é a mais simples: pegue papel e lápis e desenhe as caixas e as setas antes de rodar.
Exercícios (Beecrowd) — uma observação honesta¶
Ponteiros raramente aparecem como um tema "isolado" no Beecrowd — eles são uma ferramenta, e sua força aparece quando combinados com funções (Semana 11), vetores (Semana 07) e strings (Semana 08). Por isso, a sugestão desta semana é revisitar exercícios que você já resolveu e reescrevê-los usando ponteiros onde fizer sentido (por exemplo, uma função que preenche uma variável via ponteiro). O foco de verdade, hoje, são os desafios do zero logo abaixo.
| Nº | Problema (já visto) | Como revisitar com ponteiros |
|---|---|---|
| 1001 | Extremamente Básico | leia os dois valores e imprima o endereço de cada variável com %p. |
| 1006 | Média 1 | escreva uma função void media(double a, double b, double *r) que grava a média em *r. |
| 1116 | Dividindo Valores | pratique uma função que devolve resultado via ponteiro (prévia da Semana 11). |
(Não se preocupe se a versão "com ponteiros" parecer mais trabalhosa agora — o objetivo é praticar o mecanismo, não "resolver mais rápido". A vantagem real chega na Semana 11.)
Desafios para escrever do zero¶
- Retrato da memória: declare um
int, umdoublee umchar. Imprima, para cada um, o valor e o endereço (%pcom(void *)). Rode duas vezes e observe se os endereços mudam entre as execuções. Por que mudam? - Mexendo por ponteiro: declare
int x = 10;eint *p = &x;. Imprimaxe*p(devem ser iguais). Faça*p = 20;, imprimaxde novo e confirme que mudou. Explique, por escrito, por quê. - A função que zera: escreva
void zera(int *p)que faz*p = 0;. Namain, crieint n = 77;, chamezera(&n);e confirme quenvirou0. (Este é o embrião da passagem por referência — guarde-o para a Semana 11.) - Prevendo a saída: sem rodar, escreva no papel a saída do trecho abaixo; depois
rode e confira.
(Dica: acompanhe, linha a linha, para onde
int a = 3, b = 7; int *p = &a; *p = *p + 5; // linha 1 p = &b; // linha 2 *p = a; // linha 3 printf("a=%d b=%d\n", a, b);paponta e qual caixa cada*paltera.)
Para investigar (curiosidade prática)¶
- Tamanho dos ponteiros: imprima
sizeof(int *),sizeof(double *),sizeof(char *)e tambémsizeof(int)esizeof(double), todos com%zu. O que você nota sobre o tamanho dos ponteiros comparados entre si? E comparados com os tipos apontados? Relacione com a curiosidade da seção 8. - O
NULL, com cuidado: escrevaint *p = NULL;e tenteprintf("%d\n", *p);. Salve tudo antes — o programa vai quebrar com segmentation fault. Observe a mensagem, então corrija protegendo comif (p != NULL). Entender o crash uma vez, de propósito, vale mais do que dez avisos.
✅ Checklist de autoavaliação¶
Marque com sinceridade — se algum item não estiver ✅, volte à seção correspondente. Em ponteiros, "mais ou menos entendi" costuma virar bug depois; seja rigoroso(a).
- Explico que a memória é uma sequência de bytes, cada um com seu endereço.
- Sei nomear e situar as regiões código, globais/estáticas, heap e pilha, e ligo a pilha à pilha de chamadas da Semana 04.
- Uso o
¶ obter o endereço e o%ppara imprimi-lo — e explico por que oscanfsempre pediu&. - Defino, com minhas palavras, o que é um ponteiro e declaro com
tipo *p. - Faço um ponteiro apontar para uma variável (
int *p = &x;). - Uso
*ppara ler e alterar o valor apontado, e sei que*p = 20;mudax. - Não confundo o
*da declaração com o*da desreferência, nempcom*p. - Inicializo ponteiros (com
&xouNULL) e nunca desreferencioNULL. - Sei dizer, de cabeça, o que são
&x,p,*pe&p.
🔗 Referências e para se aprofundar¶
- Material 2026.1 — Aula 09: Estrutura de Memória e Aula 10: Memória Estática e Heap.
- BACKES, André. Linguagem C: completa e descomplicada (capítulos de ponteiros e gerenciamento de memória).
- KERNIGHAN, B.; RITCHIE, D. The C Programming Language (o clássico "K&R"), cap. 5 (Pointers and Arrays) — a referência definitiva, leia com calma.
- Documentação de referência: cppreference — Pointer declaration (avançado; bom para consultar detalhes).
- Prática: Beecrowd.
Antes de seguir para a Semana 11
Você acabou de aprender o vocabulário dos ponteiros: endereço (&), ponteiro
(tipo *p) e desreferência (*p). Isso ainda parece "para quê tudo isso?" — e é
natural. A resposta vem na Semana 11: com ponteiros, uma
função finalmente poderá alterar a variável de quem a chamou (a passagem por
referência), resolvendo aquela limitação da Semana 04 (a função só devolvia um
valor com return). Aquele & do scanf, que te acompanha desde a primeira aula, vai,
enfim, revelar todo o seu poder. Reserve tempo para digitar os exemplos desta semana:
quem constrói a base agora atravessa a Unidade 3 com tranquilidade.