Ponteiros para structs e o operador ->

1. Introdução aos Ponteiros para Structs

Em C, structs permitem agrupar dados heterogêneos em uma única unidade lógica. Quando trabalhamos com structs grandes ou precisamos modificar seus conteúdos dentro de funções, os ponteiros para structs tornam-se essenciais.

A motivação principal para usar ponteiros para structs é dupla:

  1. Eficiência: Passar uma struct por valor para uma função copia todos os seus bytes. Para structs grandes, isso é custoso. Um ponteiro (tipicamente 4 ou 8 bytes) é muito mais leve.
  2. Modificação in-place: Com ponteiros, podemos alterar diretamente os campos da struct original, sem precisar retornar uma cópia modificada.

A declaração básica é simples:

struct Pessoa {
    char nome[50];
    int idade;
};

struct Pessoa *ptr;  // Ponteiro para struct Pessoa

A diferença fundamental entre trabalhar com struct por valor e por ponteiro está no acesso aos membros:

struct Pessoa p1 = {"Ana", 25};
struct Pessoa *p2 = &p1;

// Acesso por valor
p1.idade = 26;

// Acesso por ponteiro (requer operador especial)
(*p2).idade = 27;   // Notação válida, mas verbosa
p2->idade = 27;     // Notação preferida com operador ->

2. Alocação Dinâmica de Structs

A alocação dinâmica permite criar structs em tempo de execução, armazenando-as na heap. Isso é fundamental quando não sabemos antecipadamente quantas structs serão necessárias.

#include <stdio.h>
#include <stdlib.h>

struct Pessoa {
    char nome[50];
    int idade;
};

int main() {
    struct Pessoa *ptr;

    // Aloca memória para uma struct Pessoa na heap
    ptr = (struct Pessoa*) malloc(sizeof(struct Pessoa));

    // Verificação obrigatória de falha na alocação
    if (ptr == NULL) {
        printf("Falha na alocação de memória!\n");
        return 1;
    }

    // Uso da struct alocada
    ptr->idade = 30;

    // Liberação da memória
    free(ptr);
    ptr = NULL;  // Boa prática: evitar dangling pointer

    return 0;
}

A função malloc() recebe o número de bytes a alocar. Usamos sizeof(struct Pessoa) para garantir o tamanho correto, independentemente de alinhamento ou padding. Sempre verifique se o retorno é NULL, indicando falha de alocação.

3. Acessando Membros via Ponteiro: Operador ->

O operador seta (->) é açúcar sintático para acessar membros de uma struct através de um ponteiro. A expressão ptr->membro é equivalente a (*ptr).membro, mas mais legível e menos propensa a erros de parênteses.

struct Endereco {
    char rua[100];
    int numero;
};

struct Pessoa {
    char nome[50];
    struct Endereco endereco;
};

int main() {
    struct Pessoa *p = (struct Pessoa*) malloc(sizeof(struct Pessoa));

    // Acessando campos simples
    p->idade = 25;

    // Acessando struct aninhada
    p->endereco.numero = 123;  // Operador . para membro aninhado

    // Equivalente sem operador ->
    (*p).endereco.numero = 123;

    free(p);
    return 0;
}

Note que -> é usado apenas quando temos um ponteiro para a struct. Se a struct aninhada for acessada através de um ponteiro, usamos -> novamente: p->enderecoPtr->numero.

4. Passagem de Structs para Funções

A passagem por valor cria uma cópia completa da struct, enquanto a passagem por ponteiro permite modificar a struct original.

#include <stdio.h>

struct Ponto {
    int x;
    int y;
};

// Passagem por valor - NÃO modifica o original
void moverPorValor(struct Ponto p) {
    p.x += 10;
    p.y += 10;
}

// Passagem por ponteiro - MODIFICA o original
void moverPorPonteiro(struct Ponto *p) {
    p->x += 10;
    p->y += 10;
}

// Função que retorna ponteiro para struct alocada dinamicamente
struct Ponto* criarPonto(int x, int y) {
    struct Ponto *novo = (struct Ponto*) malloc(sizeof(struct Ponto));
    if (novo != NULL) {
        novo->x = x;
        novo->y = y;
    }
    return novo;
}

int main() {
    struct Ponto p1 = {5, 10};

    moverPorValor(p1);
    printf("Após valor: (%d, %d)\n", p1.x, p1.y);  // (5, 10) - inalterado

    moverPorPonteiro(&p1);
    printf("Após ponteiro: (%d, %d)\n", p1.x, p1.y);  // (15, 20) - modificado

    struct Ponto *p2 = criarPonto(30, 40);
    if (p2 != NULL) {
        printf("Criado: (%d, %d)\n", p2->x, p2->y);
        free(p2);
    }

    return 0;
}

5. Ponteiros para Structs e Arrays

Ponteiros para structs funcionam perfeitamente com aritmética de ponteiros quando combinados com arrays.

#include <stdio.h>

struct Aluno {
    char nome[30];
    float nota;
};

int main() {
    struct Aluno turma[3] = {
        {"João", 8.5},
        {"Maria", 9.0},
        {"Pedro", 7.5}
    };

    struct Aluno *ptr = turma;  // Aponta para o primeiro elemento

    // Percorrendo o array com aritmética de ponteiros
    for (int i = 0; i < 3; i++) {
        printf("Aluno: %s, Nota: %.1f\n", ptr->nome, ptr->nota);
        ptr++;  // Avança para o próximo elemento
    }

    // Equivalente usando índices
    for (int i = 0; i < 3; i++) {
        printf("Aluno: %s, Nota: %.1f\n", turma[i].nome, turma[i].nota);
    }

    return 0;
}

Cuidado com limites do array! A aritmética de ponteiros não verifica se você ultrapassou o array alocado. Isso pode causar acesso a memória inválida e segmentation fault.

6. Ponteiros para Structs Dentro de Structs

Structs podem conter ponteiros para outras structs, criando estruturas de dados complexas como listas encadeadas.

#include <stdio.h>
#include <stdlib.h>

struct No {
    int dado;
    struct No *proximo;  // Ponteiro para próximo nó
};

int main() {
    // Criando três nós de uma lista encadeada
    struct No *primeiro = (struct No*) malloc(sizeof(struct No));
    struct No *segundo = (struct No*) malloc(sizeof(struct No));
    struct No *terceiro = (struct No*) malloc(sizeof(struct No));

    primeiro->dado = 10;
    primeiro->proximo = segundo;

    segundo->dado = 20;
    segundo->proximo = terceiro;

    terceiro->dado = 30;
    terceiro->proximo = NULL;  // Fim da lista

    // Percorrendo a lista com acesso encadeado
    struct No *atual = primeiro;
    while (atual != NULL) {
        printf("%d -> ", atual->dado);
        atual = atual->proximo;  // Acessa próximo via ponteiro
    }
    printf("NULL\n");

    // Liberação na ordem inversa (ou use recursão)
    free(terceiro);
    free(segundo);
    free(primeiro);

    return 0;
}

O acesso encadeado atual->proximo->dado só é seguro se tivermos certeza que atual->proximo não é NULL.

7. Boas Práticas e Armadilhas Comuns

#include <stdio.h>
#include <stdlib.h>

// Uso de typedef para simplificar declarações
typedef struct {
    char nome[50];
    int idade;
} Pessoa;

int main() {
    // ARMADILHA 1: Ponteiro não inicializado
    Pessoa *ptr;  // Lixo! Aponta para lugar aleatório
    // ptr->idade = 25;  // CRASH! Segmentation fault

    // SOLUÇÃO: Sempre inicializar
    Pessoa *ptr2 = NULL;

    // ARMADILHA 2: Esquecer de verificar malloc
    Pessoa *p = (Pessoa*) malloc(sizeof(Pessoa));
    // if (p == NULL) { tratamento de erro }

    // ARMADILHA 3: Memory leak - esquecer free()
    p = (Pessoa*) malloc(sizeof(Pessoa));
    // ... uso ...
    // free(p) esquecido!

    // Boa prática: após free, atribuir NULL
    free(p);
    p = NULL;

    // ARMADILHA 4: Acessar após free (dangling pointer)
    // p->idade = 30;  // Comportamento indefinido!

    return 0;
}

8. Exemplo Completo: Cadastro de Alunos

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char nome[50];
    int idade;
    float notas[3];
} Aluno;

// Função para criar aluno dinamicamente
Aluno* criarAluno(const char *nome, int idade, float n1, float n2, float n3) {
    Aluno *novo = (Aluno*) malloc(sizeof(Aluno));
    if (novo == NULL) {
        printf("Erro: memória insuficiente!\n");
        return NULL;
    }

    strcpy(novo->nome, nome);
    novo->idade = idade;
    novo->notas[0] = n1;
    novo->notas[1] = n2;
    novo->notas[2] = n3;

    return novo;
}

// Função para exibir dados usando operador ->
void exibirAluno(const Aluno *a) {
    if (a == NULL) return;

    printf("Nome: %s\n", a->nome);
    printf("Idade: %d\n", a->idade);
    printf("Notas: %.1f, %.1f, %.1f\n", a->notas[0], a->notas[1], a->notas[2]);

    float media = (a->notas[0] + a->notas[1] + a->notas[2]) / 3.0;
    printf("Média: %.2f\n", media);
}

// Função para liberar memória
void liberarAluno(Aluno **a) {
    if (a == NULL || *a == NULL) return;

    free(*a);
    *a = NULL;  // Evita dangling pointer no chamador
}

int main() {
    Aluno *aluno1 = criarAluno("Carlos Silva", 20, 8.5, 7.0, 9.2);
    Aluno *aluno2 = criarAluno("Ana Souza", 22, 6.5, 8.0, 7.8);

    if (aluno1 != NULL) {
        printf("=== Aluno 1 ===\n");
        exibirAluno(aluno1);
    }

    if (aluno2 != NULL) {
        printf("\n=== Aluno 2 ===\n");
        exibirAluno(aluno2);
    }

    // Liberação segura
    liberarAluno(&aluno1);
    liberarAluno(&aluno2);

    return 0;
}

Referências