Na aula anterior, aprendemos que structs são copiadas por inteiro quando passadas a funções. Isso é seguro, mas nem sempre é o que queremos: às vezes precisamos que a função altere a struct original, e às vezes a struct é grande demais para copiar a cada chamada. A solução é a mesma que já usamos para variáveis simples — passar o endereço. Hoje unimos structs e ponteiros, e aprendemos o operador que nasceu justamente dessa união: a seta, ->. É também a peça que faltava para, na Fase 5, construirmos listas encadeadas e árvores.
Um ponteiro para uma struct
Assim como um ponteiro pode apontar para um int, ele pode apontar para uma struct. A declaração segue a mesma lógica: o tipo, um asterisco, o nome:
#include <stdio.h>
struct Ponto { int x, y; };
int main(void) {
struct Ponto p = {3, 4};
struct Ponto *ptr = &p; // ptr aponta para p
// Acesso via desreferência explícita — funciona, mas é deselegante:
printf("x = %d\n", (*ptr).x); // 3
return 0;
}
Para acessar um campo através do ponteiro, precisamos primeiro desreferenciar (*ptr, que nos dá a struct) e depois acessar o campo (.x). Daí o (*ptr).x. Os parênteses são obrigatórios, porque o operador . tem precedência maior que o * — sem eles, *ptr.x seria interpretado como *(ptr.x), o que não faz sentido e não compila. Essa sintaxe é tão comum e tão desajeitada que o C criou um atalho para ela.
A seta: o atalho que você vai usar o tempo todo
O operador -> (a "seta") faz exatamente o que (*ptr).campo faz, mas de forma limpa e legível:
#include <stdio.h>
struct Ponto { int x, y; };
int main(void) {
struct Ponto p = {3, 4};
struct Ponto *ptr = &p;
printf("x = %d, y = %d\n", ptr->x, ptr->y); // 3, 4 — muito melhor!
ptr->x = 10; // também serve para escrever
printf("p.x agora = %d\n", p.x); // 10 — alteramos p através do ponteiro
return 0;
}
ptr->x é idêntico a (*ptr).x: "siga o ponteiro até a struct e me dê o campo x". Grave esta equivalência, porque a seta está em toda parte no C real. A regra prática é simples: quando você tem a struct diretamente, use o ponto (p.x); quando você tem um ponteiro para a struct, use a seta (ptr->x). E note, no exemplo, que ptr->x = 10 alterou o p original — porque o ponteiro dá acesso ao objeto real, não a uma cópia.
Alterando structs em funções
Agora resolvemos o problema deixado em aberto na aula passada: fazer uma função modificar a struct do chamador. Passamos o endereço da struct, e a função trabalha sobre o original via seta:
#include <stdio.h>
struct Conta {
char titular[50];
double saldo;
};
// recebe um PONTEIRO: altera o original
void depositar(struct Conta *c, double valor) {
c->saldo += valor; // seta: modifica o saldo da conta real
}
void sacar(struct Conta *c, double valor) {
if (valor <= c->saldo) {
c->saldo -= valor;
} else {
printf("Saldo insuficiente.\n");
}
}
int main(void) {
struct Conta minha = {"Marcelo", 500.0};
depositar(&minha, 250.0); // passamos o ENDEREÇO
printf("Saldo: %.2f\n", minha.saldo); // 750.00 — mudou de verdade
sacar(&minha, 1000.0); // Saldo insuficiente
sacar(&minha, 300.0);
printf("Saldo: %.2f\n", minha.saldo); // 450.00
return 0;
}
Compare com a versão da aula anterior, que recebia a struct por valor e precisava retornar a versão modificada. Aqui, depositar recebe struct Conta *c e altera o original diretamente — não há retorno, não há reatribuição. Esse é o estilo idiomático quando a função precisa mudar o estado do objeto: passa-se o ponteiro. É exatamente como fizemos com int * lá na aula de ponteiros, só que agora o alvo é uma struct inteira.
A economia: evitando cópias caras
Há um segundo motivo, além da alteração, para passar structs por ponteiro: desempenho. Uma struct grande — digamos, com um vetor de mil elementos dentro — seria copiada por inteiro a cada chamada se passada por valor. Passá-la por ponteiro copia apenas o endereço (8 bytes), não importa o tamanho da struct:
#include <stdio.h>
struct Grande {
int dados[1000]; // 4000 bytes
int total;
};
// por VALOR: copiaria 4004 bytes a cada chamada — desperdício
long soma_lenta(struct Grande g) { /* ... */ return 0; }
// por PONTEIRO: copia só 8 bytes (o endereço), mesmo com a struct enorme
long soma_rapida(const struct Grande *g) {
long s = 0;
for (int i = 0; i < g->total; i++) {
s += g->dados[i];
}
return s;
}
int main(void) {
struct Grande g = {.total = 3, .dados = {10, 20, 30}};
printf("%ld\n", soma_rapida(&g)); // 60
return 0;
}
Repare no const struct Grande *g: passamos por ponteiro para eficiência, mas usamos const para prometer que a função não vai alterar a struct — o melhor dos dois mundos. Ela ganha a economia da passagem por ponteiro sem abrir mão da segurança de não modificar o original. Esse padrão — const T * para leitura eficiente — é um dos mais idiomáticos e valiosos do C. Use-o sempre que uma função só precisa ler uma struct que não seja minúscula.
Structs no heap
Combinando com a Fase 2, podemos alocar structs dinamicamente no heap. Isso é a base absoluta das estruturas de dados que veremos adiante:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Pessoa {
char nome[50];
int idade;
};
int main(void) {
// aloca uma struct Pessoa no heap
struct Pessoa *p = malloc(sizeof(struct Pessoa));
if (p == NULL) return 1;
// preenchemos via seta, pois p é um ponteiro
strcpy(p->nome, "Ana");
p->idade = 30;
printf("%s, %d anos\n", p->nome, p->idade);
free(p); // toda struct alocada precisa ser liberada
return 0;
}
Note dois detalhes já familiares. Usamos sizeof(struct Pessoa) para pedir a malloc a quantidade exata de bytes — deixando o compilador calcular o tamanho da struct para nós. E, como p é um ponteiro, acessamos os campos com a seta (p->nome, p->idade). Ao final, um free, porque a lei do heap não muda: toda alocação precisa de sua liberação. Guarde bem esta imagem — uma struct no heap, acessada por seta, liberada com free — porque ela é literalmente o átomo de que são feitas as listas encadeadas.
O que vem a seguir
Hoje unimos os dois grandes temas do curso até aqui — structs e ponteiros — e ganhamos o operador seta (->), que torna o acesso via ponteiro limpo e natural. Aprendemos a passar structs por ponteiro para alterá-las e para evitar cópias caras (com const quando é só leitura), e a alocá-las no heap. Na próxima aula, completamos o trio de tipos definidos pelo usuário: as unions, que permitem que um mesmo espaço de memória guarde diferentes tipos em momentos diferentes, e os enums, que dão nomes legíveis a conjuntos de valores. São ferramentas de economia e de clareza que deixam seu código mais expressivo.
Fontes e leituras recomendadas
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 6.2 e 6.3, sobre structs e ponteiros para structs
- cppreference — Member access operators (
.e->) — https://en.cppreference.com/w/c/language/operator_member_access - Modern C, Jens Gustedt — seção sobre ponteiros para agregados — https://gustedt.gitlabpages.inria.fr/modern-c/
- CERT C — EXP32-C, sobre acesso correto a objetos por ponteiro — https://wiki.sei.cmu.edu/confluence/display/c
- Understanding and Using C Pointers, Richard Reese (O'Reilly) — capítulo sobre ponteiros e structs
Exercícios
Exercício 1
Defina uma struct Ponto { int x, y; }, crie uma variável e um ponteiro para ela. Imprima o campo x de duas formas equivalentes: com (*ptr).x e com ptr->x, confirmando que dão o mesmo resultado.
Ver resposta
✓ Resposta:
#include <stdio.h>
struct Ponto { int x, y; };
int main(void) {
struct Ponto p = {7, 9};
struct Ponto *ptr = &p;
printf("%d\n", (*ptr).x); // 7
printf("%d\n", ptr->x); // 7 — idêntico
return 0;
}
ptr->x é apenas uma forma mais legível de escrever (*ptr).x; ambas seguem o ponteiro até a struct e acessam o campo.
Exercício 2
Escreva uma função void mover(struct Ponto *p, int dx, int dy) que desloque um ponto somando dx a x e dy a y, alterando o ponto original. Teste e confirme a mudança.
Ver resposta
✓ Resposta:
#include <stdio.h>
struct Ponto { int x, y; };
void mover(struct Ponto *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
int main(void) {
struct Ponto p = {1, 1};
mover(&p, 4, 5);
printf("(%d, %d)\n", p.x, p.y); // (5, 6)
return 0;
}
Como passamos &p (o endereço), a função altera o ponto original via seta.
Exercício 3
Defina uma struct Retangulo { double largura, altura; }. Escreva uma função double area(const struct Retangulo *r) que receba a struct por ponteiro constante e retorne a área. Explique por que usamos const aqui.
Ver resposta
✓ Resposta:
#include <stdio.h>
struct Retangulo { double largura, altura; };
double area(const struct Retangulo *r) {
return r->largura * r->altura;
}
int main(void) {
struct Retangulo caixa = {4.0, 3.0};
printf("%.1f\n", area(&caixa)); // 12.0
return 0;
}
Usamos const porque area só precisa ler a struct, não modificá-la. O const comunica essa intenção ao leitor e faz o compilador garantir que a função não altere r por engano. Passamos por ponteiro (em vez de por valor) para evitar copiar a struct, e o const recupera a segurança que a passagem por valor daria de graça.
Exercício 4
Aloque uma struct Pessoa { char nome[50]; int idade; } no heap com malloc, preencha seus campos via seta, imprima-os e libere a memória. Não esqueça de verificar o retorno de malloc.
Ver resposta
✓ Resposta:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Pessoa { char nome[50]; int idade; };
int main(void) {
struct Pessoa *p = malloc(sizeof(struct Pessoa));
if (p == NULL) return 1;
strcpy(p->nome, "Bruno");
p->idade = 25;
printf("%s, %d anos\n", p->nome, p->idade);
free(p);
return 0;
}
Como p é um ponteiro, os campos são acessados com a seta; ao final, free(p) devolve a memória alocada.
Exercício 5
Explique, com suas palavras, quando você deve usar o operador ponto (.) e quando deve usar a seta (->). Dê um exemplo de cada.
Ver resposta
✓ Resposta: Use o ponto (.) quando você tem a struct diretamente, como um valor: struct Ponto p; p.x = 5;. Use a seta (->) quando você tem um ponteiro para a struct: struct Ponto *ptr = &p; ptr->x = 5;. A regra mnemônica: se o nome à esquerda é um ponteiro, use a seta; se é a própria struct, use o ponto. A seta, na verdade, é só um atalho para "desreferenciar e acessar o campo" — ptr->x equivale a (*ptr).x.