Nesta aula, vamos explorar a combinação de structs e ponteiros, um tópico fundamental para manipular dados de forma eficiente em C. Entender como acessar membros de uma struct através de um ponteiro, alocar structs dinamicamente na heap e passar structs para funções por referência permite escrever código mais flexível e com melhor desempenho.

Operador ->

Quando temos um ponteiro para uma struct, o operador de seta (->) é usado para acessar os membros da struct apontada. Sem ele, precisaríamos desreferenciar o ponteiro e depois usar o operador ponto, o que é mais verboso. O operador -> combina desreferência e acesso a membro em um só passo.

Por exemplo, se p é um ponteiro para uma struct do tipo Pessoa, então p->nome é equivalente a (*p).nome. O operador -> é mais legível e é a forma preferida em C.

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

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

int main() {
    Pessoa p1 = {"Alice", 30};
    Pessoa *ptr = &p1;

    // Usando operador ->
    printf("Nome: %s\n", ptr->nome);
    printf("Idade: %d\n", ptr->idade);

    // Equivalente com desreferência
    printf("Nome: %s\n", (*ptr).nome);
    printf("Idade: %d\n", (*ptr).idade);

    return 0;
}

Structs na heap

Alocar structs dinamicamente na heap (com malloc, calloc ou realloc) é útil quando o número de elementos não é conhecido em tempo de compilação ou quando queremos que a struct persista além do escopo da função. A alocação na heap retorna um ponteiro para a memória alocada, que deve ser liberada com free quando não for mais necessária.

Para acessar os membros de uma struct alocada na heap, usamos o operador ->. É importante verificar se a alocação foi bem-sucedida (ponteiro não nulo).

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

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

int main() {
    Pessoa *ptr = (Pessoa*) malloc(sizeof(Pessoa));
    if (ptr == NULL) {
        printf("Erro de alocação\n");
        return 1;
    }

    // Atribuindo valores
    strcpy(ptr->nome, "Bob");
    ptr->idade = 25;

    printf("Nome: %s, Idade: %d\n", ptr->nome, ptr->idade);

    free(ptr);
    return 0;
}

Passando structs para funções

Structs podem ser passadas para funções por valor (cópia) ou por referência (ponteiro). Passar por valor é simples, mas para structs grandes pode ser ineficiente e a função não pode modificar a struct original. Passar por referência (um ponteiro) é mais eficiente e permite modificar a struct original.

Quando passamos por referência, usamos o operador -> para acessar os membros. A função recebe um ponteiro e pode alterar o conteúdo da struct original.

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

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

// Passagem por valor (cópia)
void imprimePessoa(Pessoa p) {
    printf("Nome: %s, Idade: %d\n", p.nome, p.idade);
}

// Passagem por referência (ponteiro)
void atualizaIdade(Pessoa *p, int novaIdade) {
    p->idade = novaIdade;  // Modifica a struct original
}

int main() {
    Pessoa p1 = {"Charlie", 40};
    imprimePessoa(p1);

    atualizaIdade(&p1, 45);
    imprimePessoa(p1);  // Agora idade é 45

    return 0;
}

Exemplos

Vamos ver um exemplo mais completo que combina structs, ponteiros, alocação dinâmica e funções. Criaremos uma função que inicializa uma struct alocada na heap e outra que a libera.

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

typedef struct {
    char *nome;  // Ponteiro para string alocada dinamicamente
    int idade;
} Pessoa;

// Função que cria uma nova Pessoa na heap
Pessoa* criaPessoa(const char *nome, int idade) {
    Pessoa *p = (Pessoa*) malloc(sizeof(Pessoa));
    if (p == NULL) return NULL;

    p->nome = (char*) malloc(strlen(nome) + 1);
    if (p->nome == NULL) {
        free(p);
        return NULL;
    }
    strcpy(p->nome, nome);
    p->idade = idade;
    return p;
}

// Função que libera a memória da Pessoa
void liberaPessoa(Pessoa *p) {
    if (p != NULL) {
        free(p->nome);
        free(p);
    }
}

int main() {
    Pessoa *p = criaPessoa("Diana", 35);
    if (p == NULL) {
        printf("Erro ao criar pessoa\n");
        return 1;
    }
    printf("Nome: %s, Idade: %d\n", p->nome, p->idade);
    liberaPessoa(p);
    return 0;
}

Boas práticas e observações finais

Sempre verifique o retorno de malloc e funções de alocação para evitar erros. Libere a memória alocada com free quando não for mais usada. Prefira passar structs por referência quando a função precisar modificar a struct ou quando a struct for grande, para evitar cópias desnecessárias. O operador -> é mais legível e seguro que (*ptr).membro.

Referências

Exercícios

  1. Declare uma struct Ponto com campos x e y (int). Crie uma função que receba um ponteiro para Ponto e imprima as coordenadas no formato "(x, y)".

    ✓ Resposta:
    #include <stdio.h>
    
    typedef struct {
        int x;
        int y;
    } Ponto;
    
    void imprimePonto(Ponto *p) {
        printf("(%d, %d)\n", p->x, p->y);
    }
    
    int main() {
        Ponto p = {10, 20};
        imprimePonto(&p);
        return 0;
    }
    
  2. Escreva uma função que aloque dinamicamente uma struct Livro (com campos titulo (string) e ano (int)) e a inicialize com valores fornecidos. A função deve retornar um ponteiro para a struct alocada.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    typedef struct {
        char titulo[100];
        int ano;
    } Livro;
    
    Livro* criaLivro(const char *titulo, int ano) {
        Livro *livro = (Livro*) malloc(sizeof(Livro));
        if (livro == NULL) return NULL;
        strcpy(livro->titulo, titulo);
        livro->ano = ano;
        return livro;
    }
    
    int main() {
        Livro *livro = criaLivro("1984", 1949);
        if (livro != NULL) {
            printf("%s (%d)\n", livro->titulo, livro->ano);
            free(livro);
        }
        return 0;
    }
    
  3. Crie uma função que receba um ponteiro para uma struct Pessoa (com nome e idade) e atualize a idade para o dobro da atual. Teste no main.

    ✓ Resposta:
    #include <stdio.h>
    
    typedef struct {
        char nome[50];
        int idade;
    } Pessoa;
    
    void dobraIdade(Pessoa *p) {
        p->idade *= 2;
    }
    
    int main() {
        Pessoa p = {"Eva", 20};
        dobraIdade(&p);
        printf("Idade: %d\n", p.idade);  // 40
        return 0;
    }
    
  4. Explique a diferença entre p->membro e (*p).membro e demonstre com um exemplo.

    ✓ Resposta:

    Ambos acessam o membro de uma struct apontada por um ponteiro. p->membro é açúcar sintático para (*p).membro. O operador -> é mais legível e comumente usado.

    #include <stdio.h>
    
    typedef struct {
        int valor;
    } Exemplo;
    
    int main() {
        Exemplo e = {42};
        Exemplo *ptr = &e;
        printf("%d\n", ptr->valor);   // 42
        printf("%d\n", (*ptr).valor); // 42
        return 0;
    }
    
  5. Crie uma struct Aluno com matrícula (int) e nota (float). Escreva uma função que receba um array de ponteiros para Aluno (alocados dinamicamente) e o tamanho, e imprima a média das notas. Não esqueça de liberar a memória.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct {
        int matricula;
        float nota;
    } Aluno;
    
    float mediaNotas(Aluno **alunos, int n) {
        float soma = 0;
        for (int i = 0; i < n; i++) {
            soma += alunos[i]->nota;
        }
        return soma / n;
    }
    
    int main() {
        int n = 3;
        Aluno **alunos = (Aluno**) malloc(n * sizeof(Aluno*));
        for (int i = 0; i < n; i++) {
            alunos[i] = (Aluno*) malloc(sizeof(Aluno));
            alunos[i]->matricula = i + 1;
            alunos[i]->nota = 7.0 + i;
        }
        printf("Média: %.2f\n", mediaNotas(alunos, n));
        for (int i = 0; i < n; i++) {
            free(alunos[i]);
        }
        free(alunos);
        return 0;
    }