Structs: agrupando dados heterogêneos

1. Introdução às structs

Em Linguagem C, arrays permitem armazenar múltiplos valores do mesmo tipo, mas frequentemente precisamos agrupar dados de tipos diferentes que representam uma entidade lógica. Por exemplo, um registro de aluno contém nome (string), idade (inteiro) e notas (floats). As structs (estruturas) são o mecanismo da linguagem para criar tipos compostos que agrupam membros heterogêneos.

A sintaxe básica para declarar uma struct é:

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

Após a declaração, podemos criar variáveis desse tipo:

struct Pessoa pessoa1;
struct Pessoa pessoa2, pessoa3;

2. Acesso e manipulação de membros

O operador ponto (.) é usado para acessar campos individuais de uma struct:

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

struct Aluno {
    char nome[50];
    int matricula;
    float nota;
};

int main() {
    struct Aluno aluno1;

    strcpy(aluno1.nome, "Maria Silva");
    aluno1.matricula = 2024001;
    aluno1.nota = 9.5;

    printf("Nome: %s\n", aluno1.nome);
    printf("Matrícula: %d\n", aluno1.matricula);
    printf("Nota: %.2f\n", aluno1.nota);

    return 0;
}

Atribuição direta entre variáveis struct realiza uma cópia rasa (shallow copy) de todos os membros:

struct Aluno aluno2 = aluno1;  // Copia todos os campos

Inicialização agregada permite definir valores na declaração:

struct Aluno aluno3 = {"João Pedro", 2024002, 8.0};

Com C99, podemos usar designadores para inicializar campos específicos:

struct Aluno aluno4 = {.nome = "Ana Costa", .nota = 7.5, .matricula = 2024003};

3. Structs aninhadas

Structs podem conter outras structs como membros, permitindo criar hierarquias de dados:

struct Endereco {
    char rua[100];
    int numero;
    char cidade[50];
    char estado[3];
};

struct Funcionario {
    char nome[50];
    int id;
    struct Endereco endereco;
    float salario;
};

int main() {
    struct Funcionario func = {"Carlos", 101, {"Av. Brasil", 300, "São Paulo", "SP"}, 5000.0};

    printf("Funcionário: %s\n", func.nome);
    printf("Cidade: %s\n", func.endereco.cidade);  // Acesso aninhado
    printf("Estado: %s\n", func.endereco.estado);

    return 0;
}

Structs também podem conter arrays como membros, como já vimos com strings. Arrays multidimensionais também são suportados:

struct Turma {
    char nome_turma[30];
    float notas[5][3];  // 5 alunos, 3 notas cada
};

4. Typedef e structs

O uso de typedef simplifica a declaração de variáveis, eliminando a necessidade da palavra-chave struct:

typedef struct {
    char titulo[100];
    char autor[50];
    int ano;
    float preco;
} Livro;

int main() {
    Livro livro1;  // Não precisa de "struct Livro"
    Livro livro2 = {"1984", "George Orwell", 1949, 39.90};

    return 0;
}

Structs anônimas com typedef são comuns em código moderno C. A legibilidade melhora significativamente, especialmente em projetos complexos. A desvantagem é que structs anônimas não podem ser referenciadas antes do typedef.

5. Ponteiros para structs

Ponteiros para structs são essenciais para manipulação eficiente e alocação dinâmica:

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

typedef struct {
    int x;
    int y;
} Ponto;

int main() {
    Ponto p1 = {10, 20};
    Ponto *ptr = &p1;

    // Operador seta (->) para acesso indireto
    printf("x = %d, y = %d\n", ptr->x, ptr->y);

    // Equivalente usando operador ponto
    printf("x = %d, y = %d\n", (*ptr).x, (*ptr).y);

    // Alocação dinâmica
    Ponto *p2 = (Ponto*) malloc(sizeof(Ponto));
    if (p2 != NULL) {
        p2->x = 30;
        p2->y = 40;
        free(p2);
    }

    return 0;
}

Passagem para funções pode ser feita por valor ou por referência:

// Por valor (cópia completa)
void imprimirPonto(Ponto p) {
    printf("(%d, %d)\n", p.x, p.y);
}

// Por referência (eficiente, permite modificação)
void moverPonto(Ponto *p, int dx, int dy) {
    p->x += dx;
    p->y += dy;
}

6. Alinhamento e padding em structs

O compilador pode inserir bytes de preenchimento (padding) entre membros para alinhamento de memória:

#include <stdio.h>
#include <stddef.h>

struct Exemplo {
    char c;      // 1 byte
    int i;       // 4 bytes (geralmente alinhado a 4)
    short s;     // 2 bytes
};

int main() {
    printf("Tamanho da struct: %zu bytes\n", sizeof(struct Exemplo));
    printf("Offset de c: %zu\n", offsetof(struct Exemplo, c));
    printf("Offset de i: %zu\n", offsetof(struct Exemplo, i));
    printf("Offset de s: %zu\n", offsetof(struct Exemplo, s));

    return 0;
}

Em muitos sistemas, essa struct ocupará 12 bytes (1 + 3 padding + 4 + 2 + 2 padding), não 7 como a soma dos membros sugere. O padding garante que cada membro esteja em um endereço múltiplo do seu tamanho.

7. Structs como tipos de retorno e parâmetros

Structs podem ser retornadas por valor de funções:

typedef struct {
    float media;
    float maior;
    float menor;
} Estatisticas;

Estatisticas calcularNotas(float notas[], int tamanho) {
    Estatisticas est = {0};
    float soma = 0;
    est.maior = est.menor = notas[0];

    for (int i = 0; i < tamanho; i++) {
        soma += notas[i];
        if (notas[i] > est.maior) est.maior = notas[i];
        if (notas[i] < est.menor) est.menor = notas[i];
    }
    est.media = soma / tamanho;
    return est;
}

Para structs grandes, a passagem por referência é mais eficiente:

void processarFuncionario(const struct Funcionario *f) {
    // Acesso somente leitura sem cópia
    printf("%s mora em %s\n", f->nome, f->endereco.cidade);
}

8. Exemplos práticos e boas práticas

Exemplo 1: Registro de aluno com nome, idade e notas

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

#define MAX_ALUNOS 100
#define NUM_NOTAS 3

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

void calcularMedia(Aluno *a) {
    float soma = 0;
    for (int i = 0; i < NUM_NOTAS; i++) {
        soma += a->notas[i];
    }
    a->media = soma / NUM_NOTAS;
}

int main() {
    Aluno alunos[MAX_ALUNOS];
    int num_alunos = 0;

    // Cadastro
    strcpy(alunos[0].nome, "Lucas");
    alunos[0].idade = 20;
    alunos[0].notas[0] = 8.5;
    alunos[0].notas[1] = 7.0;
    alunos[0].notas[2] = 9.2;
    calcularMedia(&alunos[0]);
    num_alunos++;

    printf("Aluno: %s, Média: %.2f\n", alunos[0].nome, alunos[0].media);

    return 0;
}

Exemplo 2: Lista encadeada simples com struct auto-referenciada

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

typedef struct No {
    int dado;
    struct No *proximo;  // Auto-referência
} No;

No* criarNo(int valor) {
    No *novo = (No*) malloc(sizeof(No));
    if (novo) {
        novo->dado = valor;
        novo->proximo = NULL;
    }
    return novo;
}

void inserirInicio(No **cabeca, int valor) {
    No *novo = criarNo(valor);
    if (novo) {
        novo->proximo = *cabeca;
        *cabeca = novo;
    }
}

void imprimirLista(No *cabeca) {
    while (cabeca) {
        printf("%d -> ", cabeca->dado);
        cabeca = cabeca->proximo;
    }
    printf("NULL\n");
}

int main() {
    No *lista = NULL;
    inserirInicio(&lista, 10);
    inserirInicio(&lista, 20);
    inserirInicio(&lista, 30);
    imprimirLista(lista);

    return 0;
}

Boas práticas:
- Declare structs em arquivos de cabeçalho (.h) e implemente funções em arquivos (.c)
- Use typedef para simplificar nomes, mas evite esconder ponteiros em typedefs
- Prefira passar structs grandes por ponteiro, especialmente com const quando não houver modificação
- Documente o propósito de cada campo da struct

Referências