Ponteiros para Struct e o Operador Seta

[359] Ponteiros para Struct e o Operador Seta

Os parênteses em (*ptr).x não são preciosismo: sem eles o ponto vence o asterisco e o código nem compila. A seta existe para poupar você disso — e junto dela vêm a função que altera a struct do chamador, o const que lê sem copiar, e a struct no heap, átomo de toda lista encadeada que virá adiante.
Linguagem C

9 min de leitura

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

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.

Comentários

Mais em Linguagem C

Unions e Enums: Economia e Clareza
Unions e Enums: Economia e Clareza

Uma struct reserva espaço para todos os campos ao mesmo tempo; a union, para…

Comunicação em Rede com Sockets
Comunicação em Rede com Sockets

Depois de conectado, um socket se lê e se escreve como arquivo — e é essa…

Processos: fork, exec e wait
Processos: fork, exec e wait

A linha do fork é executada uma vez e retorna duas — e é do valor diferente em…