Pular para conteúdo

Semana 14 — Registros (structs) e enumerações

Nesta semana

Unidade 3 · Seg 09/11 · Qua 11/11 · Sex 13/11

Até aqui, todas as coleções de dados que você montou eram homogêneas: um vetor de int, uma matriz de double, uma string de char. Mas o mundo real raramente cabe num único tipo. Um aluno tem nome (texto), matrícula (inteiro) e média (real) — três tipos diferentes que descrevem uma só entidade. Nesta semana aprendemos a agrupar dados heterogêneos num único "pacote" com nome: a struct (registro).

Vamos ver como definir uma struct, declarar variáveis dela, acessar seus campos com o operador ponto ., simplificar tudo com typedef, passar structs para funções (por valor e por ponteiro, com o operador seta ->), montar vetores de structs e, por fim, dar nome a conjuntos de constantes com o enum. É a semana em que você começa a modelar problemas de verdade.

🎯 Objetivos de aprendizagem

Ao final desta semana, você deverá ser capaz de:

  • explicar por que existem tipos heterogêneos e quando usar uma struct em vez de variáveis soltas ou vetores paralelos;
  • definir uma struct, declarar variáveis dela e acessar seus campos com o operador ponto (.);
  • inicializar structs e simplificar a sintaxe com typedef;
  • passar structs para funções por valor e por ponteiro, usando o operador seta (->) e entendendo o custo da cópia;
  • construir e percorrer vetores de structs para representar coleções de entidades;
  • usar enum para nomear conjuntos de constantes inteiras e empregá-las em switch, ganhando legibilidade.

📖 Segunda — Estudo do conteúdo

1. O problema: dados que não cabem num único tipo

Imagine que você precisa representar um aluno no seu programa. Um aluno tem:

  • um nome — texto, ou seja, um vetor de char (string, da Semana 08);
  • uma matrícula — um int;
  • uma média — um double.

Com o que você sabe até aqui, restam duas saídas ruins. A primeira é criar variáveis soltas:

char nome[50];
int matricula;
double media;

Funciona para um aluno. Mas e para quarenta? Você cairia na segunda saída ruim, os vetores paralelos:

char nomes[40][50];
int matriculas[40];
double medias[40];

O aluno de índice i estaria "espalhado" por três vetores: nomes[i], matriculas[i] e medias[i]. Isso é frágil: se você ordenar um vetor e esquecer os outros, os dados se desencontram — o nome de um aluno passa a apontar para a média de outro. Além disso, é impossível dizer "passe este aluno para uma função": você teria de passar três coisas.

Os vetores que você conhece são homogêneos — guardam elementos do mesmo tipo. O que falta é uma forma de agrupar dados de tipos diferentes que, juntos, descrevem uma entidade. Essa forma é a estrutura heterogênea, ou, em C, a struct.

A ideia central em uma frase

Um vetor agrupa muitos dados do mesmo tipo (homogêneo). Uma struct agrupa poucos dados de tipos diferentes que formam um todo com significado (heterogêneo). São ferramentas complementares — e, como veremos, combiná-las (um vetor de structs) é o que resolve o problema dos quarenta alunos.

2. Definindo uma struct

Uma struct ("estrutura", ou registro) é um novo tipo de dado que você cria agrupando campos (também chamados membros). Definimos o "molde":

struct Aluno {
    char nome[50];
    int matricula;
    double media;
};   // <- repare no ponto e vírgula ao final!

Isso não cria nenhuma variável ainda — cria um tipo novo, chamado struct Aluno, do mesmo jeito que int ou double são tipos. É um molde, uma planta baixa: descreve o formato de qualquer "aluno", dizendo que ele tem um nome, uma matricula e uma media.

Não esqueça o ; depois da chave } que fecha a struct

Diferentemente de um bloco de função, a definição de uma struct termina com ponto e vírgula. Esquecê-lo gera mensagens de erro confusas, muitas vezes apontando para a linha seguinte (frequentemente a main). É um dos erros mais comuns da semana.

3. Declarando variáveis e acessando campos com o operador ponto (.)

Com o molde pronto, criamos variáveis daquele tipo — cada uma é um aluno concreto:

struct Aluno a;   // 'a' é uma variável do tipo "struct Aluno"

Para acessar (ler ou escrever) um campo de a, usamos o operador ponto (.), escrevendo variavel.campo:

a.matricula = 20250123;      // escreve no campo matricula
a.media = 8.75;              // escreve no campo media
printf("%d\n", a.matricula); // lê o campo matricula

O campo nome é uma string (vetor de char), então não se atribui com =; usamos scanf/fgets ou strcpy (Semana 08):

#include <string.h>
strcpy(a.nome, "Ana Souza");   // copia o texto para o campo nome

Um programa completinho:

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

struct Aluno {
    char nome[50];
    int matricula;
    double media;
};

int main(void) {
    struct Aluno a;                 // declara uma variável do tipo struct Aluno

    strcpy(a.nome, "Ana Souza");    // preenche os campos, um a um
    a.matricula = 20250123;
    a.media = 8.75;

    printf("Nome:      %s\n", a.nome);
    printf("Matricula: %d\n", a.matricula);
    printf("Media:     %.2f\n", a.media);
    return 0;
}

4. Inicializando uma struct

Assim como uma variável simples pode ser declarada e inicializada de uma vez (int x = 5;), uma struct também pode. Basta listar os valores dos campos, na ordem em que foram definidos, entre chaves:

struct Aluno a = { "Ana Souza", 20250123, 8.75 };
//                    nome        matricula  media

O C moderno (padrão C99 em diante) também aceita inicializadores designados, em que você nomeia cada campo — mais legível e imune à ordem:

struct Aluno b = { .nome = "Bruno Lima", .matricula = 20250456, .media = 7.20 };

Se você inicializar com {0}, todos os campos ficam zerados (números viram 0, a string vira vazia) — um jeito seguro de começar "limpo":

struct Aluno vazio = {0};   // tudo zerado

Ler campos de uma struct com scanf

Cada campo é lido individualmente, respeitando o especificador do seu tipo. Lembre-se de que campos numéricos precisam do & e que double lê com %lf:

scanf("%d",  &a.matricula);   // int  -> & e %d
scanf("%lf", &a.media);       // double -> & e %lf
scanf("%s",  a.nome);         // string: SEM &, pois vetor já "é" endereço
O %s do scanfaté o primeiro espaço (para "Ana Souza" ele pegaria só "Ana"). Para nomes com espaços, use fgets(a.nome, 50, stdin);, tomando o cuidado usual com o \n que ele pode deixar no fim. Sem se alongar: escolha %s para uma palavra e fgets para uma linha inteira.

5. typedef: dando um apelido ao tipo

Escrever struct Aluno toda hora é repetitivo. O typedef cria um apelido (um nome alternativo) para um tipo. Combinado com a struct, permite declarar variáveis usando só Aluno, sem a palavra struct:

typedef struct {
    char nome[50];
    int matricula;
    double media;
} Aluno;      // 'Aluno' passa a ser o nome do tipo

A partir daí:

Aluno a;              // em vez de "struct Aluno a;"
Aluno turma[40];      // um vetor de alunos (veja a seção 7)

As duas formas produzem o mesmo resultado; compare lado a lado:

Sem typedef Com typedef
struct Aluno { ... }; typedef struct { ... } Aluno;
struct Aluno a; Aluno a;
Acesso: a.media Acesso: a.media (igual)

Nesta disciplina, adotaremos preferencialmente o typedef por deixar o código mais limpo — é como a maioria dos programas C profissionais faz.

Curiosidade 💡 — dois nomes na mesma definição

Você pode nomear a struct e criar o apelido de uma vez: typedef struct Aluno { ... } Aluno;. Aqui struct Aluno (nome da struct) e Aluno (apelido do typedef) coexistem. Isso é necessário, por exemplo, quando a struct precisa se referir a si mesma por ponteiro — o caso das listas encadeadas, que você verá mais adiante em Estruturas de Dados.

6. Structs em funções: por valor × por ponteiro (o operador ->)

Você pode passar uma struct para uma função e retorná-la, exatamente como faz com um int. Mas há um detalhe crucial, que liga diretamente com a Semana 04: por padrão, C passa argumentos por valor — ou seja, a função recebe uma cópia. Com uma struct, isso significa copiar TODOS os campos.

6.1 Por valor (a cópia)

por_valor_struct.c
#include <stdio.h>

typedef struct {
    char nome[50];
    int matricula;
    double media;
} Aluno;

// recebe uma CÓPIA do aluno; boa para apenas LER/mostrar
void imprime(Aluno x) {
    printf("%s (mat. %d) - media %.2f\n", x.nome, x.matricula, x.media);
}

// tenta alterar... mas mexe apenas na cópia local!
void tenta_aprovar(Aluno x) {
    x.media = 10.0;   // altera a CÓPIA, não o original
}

int main(void) {
    Aluno a = { "Ana", 20250123, 5.0 };
    tenta_aprovar(a);
    imprime(a);       // media continua 5.00 — o original NÃO mudou
    return 0;
}

Assim como o tenta_dobrar da Semana 04 não mudava o int original, tenta_aprovar não muda o aluno original: ela mexeu numa cópia que morreu ao fim da função. A diferença é que, agora, a cópia pode ser grande (esta struct tem mais de 60 bytes). Copiar tudo isso a cada chamada custa tempo e memória.

6.2 Por ponteiro (o operador seta ->)

Para alterar o original — e, de quebra, evitar a cópia — passamos o endereço da struct, isto é, um ponteiro para ela (ideia da Semana 11). Dentro da função, temos um ponteiro; para acessar um campo através de um ponteiro, usamos o operador seta (->):

por_ponteiro_struct.c
#include <stdio.h>

typedef struct {
    char nome[50];
    int matricula;
    double media;
} Aluno;

// recebe o ENDEREÇO do aluno; pode ALTERAR o original e não copia tudo
void aprovar(Aluno *p) {
    p->media = 10.0;        // p->media é atalho para (*p).media
}

int main(void) {
    Aluno a = { "Ana", 20250123, 5.0 };
    aprovar(&a);            // passa o ENDEREÇO de 'a' (repare no &)
    printf("%.2f\n", a.media);   // agora imprime 10.00 — o original mudou!
    return 0;
}

Repare no padrão: . quando temos a struct diretamente (a.media); -> quando temos um ponteiro para a struct (p->media). Resumindo:

Você tem... Para acessar o campo, use Exemplo
a struct em si (Aluno a) ponto . a.media
um ponteiro para struct (Aluno *p) seta -> p->media

Curiosidade 💡 — a seta -> é só um atalho

p->media é exatamente o mesmo que (*p).media: primeiro "desreferenciamos" o ponteiro com *p (para chegar à struct apontada) e depois pegamos o campo com .. Os parênteses são obrigatórios porque o . tem precedência maior que o *. Como esse padrão é pouco legível e usadíssimo, a linguagem criou a seta -> como abreviação. As duas escritas geram o mesmo código; use ->, é o idiomático.

Quando passar por valor e quando por ponteiro?

  • Só vai ler a struct e ela é pequena? Por valor está ótimo (é seguro: a função não estraga o original).
  • Precisa alterar o original, ou a struct é grande (evitar o custo da cópia)? Passe por ponteiro. Se for por ponteiro mas só para ler, use const Aluno *p para deixar claro (e o compilador garantir) que a função não altera.

6.3 Retornando uma struct

Uma função também pode devolver uma struct inteira com return — útil para "montar" e entregar uma entidade pronta:

Aluno cria_aluno(const char *nome, int mat, double media) {
    Aluno novo;
    strcpy(novo.nome, nome);
    novo.matricula = mat;
    novo.media = media;
    return novo;            // devolve uma cópia do aluno montado
}

7. Vetor de structs: a solução para "muitas entidades"

Agora fechamos o ciclo aberto na seção 1. Para representar uma turma inteira, combinamos as duas ideias: um vetor cujos elementos são structs.

Aluno turma[40];   // 40 alunos, cada um com seus três campos

Cada turma[i] é um Aluno completo; para chegar a um campo, encadeamos: primeiro o índice (turma[i]), depois o ponto e o campo (turma[i].media). Percorremos com um laço, como qualquer vetor:

turma.c
#include <stdio.h>

typedef struct {
    char nome[50];
    int matricula;
    double media;
} Aluno;

int main(void) {
    int n;
    Aluno turma[40];

    printf("Quantos alunos? ");
    scanf("%d", &n);

    for (int i = 0; i < n; i++) {
        printf("Aluno %d - nome: ", i + 1);
        scanf("%s", turma[i].nome);          // string: sem &
        printf("Aluno %d - matricula: ", i + 1);
        scanf("%d", &turma[i].matricula);    // int: com &
        printf("Aluno %d - media: ", i + 1);
        scanf("%lf", &turma[i].media);       // double: com & e %lf
    }

    // percorre e mostra os aprovados (media >= 7.0)
    printf("\nAprovados:\n");
    for (int i = 0; i < n; i++) {
        if (turma[i].media >= 7.0) {
            printf("  %s (%.2f)\n", turma[i].nome, turma[i].media);
        }
    }
    return 0;
}

Compare com os vetores paralelos da seção 1: aqui, os dados de cada aluno andam sempre juntos. Ordenar a turma por média, por exemplo, move o registro inteiro de uma vez — nome, matrícula e média nunca se desencontram.

8. Enumerações (enum): dando nome a constantes

Muitas vezes precisamos de um conjunto fixo e nomeado de valores relacionados: os dias da semana, os naipes de um baralho, os estados de um pedido (pendente, pago, enviado). Você até poderia usar inteiros "mágicos" — 0 para domingo, 1 para segunda... — mas o código fica ilegível: o que significa if (dia == 3)?

O enum ("enumeração") resolve isso: cria um tipo cujos valores são nomes para constantes inteiras.

enum DiaSemana { DOM, SEG, TER, QUA, QUI, SEX, SAB };

Por baixo, cada nome é um int: por padrão, DOM vale 0, SEG vale 1, ..., SAB vale 6. Mas no código você escreve o nome, não o número — e isso muda tudo em legibilidade:

enum DiaSemana hoje = QUA;

if (hoje == SAB || hoje == DOM) {
    printf("Fim de semana!\n");
} else {
    printf("Dia util.\n");
}

O enum combina lindamente com o switch (da Semana 02), porque os case ficam autoexplicativos:

dia.c
#include <stdio.h>

typedef enum { DOM, SEG, TER, QUA, QUI, SEX, SAB } DiaSemana;

int main(void) {
    DiaSemana hoje = SEX;

    switch (hoje) {
        case SAB:
        case DOM:
            printf("Descanso!\n");
            break;
        case SEX:
            printf("Sextou!\n");
            break;
        default:
            printf("Dia de estudo.\n");
            break;
    }
    return 0;
}

Repare que usamos typedef também com o enum, do mesmo jeito que fizemos com a struct — para escrever só DiaSemana em vez de enum DiaSemana.

Curiosidade 💡 — por baixo, um enum é um int

Um enum é, internamente, apenas um int. Por padrão a contagem começa em 0 e incrementa de 1 em 1, mas você pode fixar valores à mão:

enum Naipe { PAUS = 1, OUROS, COPAS, ESPADAS };  // 1, 2, 3, 4
enum HTTP  { OK = 200, NOT_FOUND = 404 };        // valores "salteados"
Quando você atribui um valor, os nomes seguintes continuam incrementando a partir dele (OUROS vira 2). Como no fundo é um int, printf("%d", COPAS); imprime 3 — o enumnome; o valor continua sendo um número.

9. Structs aninhadas e o tamanho na memória

Um campo de uma struct pode ser, ele próprio, outra struct. Isso é natural: uma data tem dia, mês e ano; um aluno tem uma data de nascimento. Chamamos isso de structs aninhadas:

typedef struct {
    int dia, mes, ano;
} Data;

typedef struct {
    char nome[50];
    int matricula;
    double media;
    Data nascimento;      // um campo que é, ele mesmo, uma struct!
} Aluno;

Para chegar a um campo da struct interna, encadeamos os pontos:

Aluno a;
a.nascimento.dia = 15;    // campo 'dia' da 'nascimento' do aluno 'a'
a.nascimento.mes = 3;
a.nascimento.ano = 2005;

Curiosidade 💡 — sizeof de uma struct pode ser MAIOR que a soma dos campos

Você imaginaria que uma struct com um int (4 bytes) e um char (1 byte) ocupasse 5 bytes. Frequentemente ela ocupa 8! Isso se chama alinhamento (padding): por eficiência de acesso, o processador prefere que certos tipos comecem em endereços "redondos" (múltiplos do seu tamanho). O compilador então insere bytes de preenchimento invisíveis entre os campos para respeitar esse alinhamento. Por isso, a regra é: nunca assuma que sizeof(struct) == soma dos sizeof dos campos. Meça com sizeof — é justamente o que sugerimos investigar na sexta-feira.

10. Erros comuns desta semana (resumão)

Checklist de armadilhas

  • Esquecer o ; depois do } que fecha a definição da struct/enum.
  • Usar . onde deveria ser -> (com ponteiro para struct) — e vice-versa.
  • Tentar copiar uma string de um campo com = (a.nome = "Ana";) em vez de strcpy.
  • Esperar que uma struct passada por valor seja alterada pela função (é cópia!).
  • No scanf, esquecer o & em campos numéricos (&a.media) ou usá-lo no campo string (o vetor a.nome já é um endereço).
  • Usar %f (em vez de %lf) para ler um campo double.
  • Confundir a definição da struct (o molde, uma vez) com a declaração de variáveis (quantas quiser).
  • Assumir que sizeof(struct) é a soma dos campos (esquecer o padding).

👥 Quarta — Encontro

Chegue ao encontro tendo estudado o material acima e tendo executado os exemplos. O encontro é o momento de consolidar e tirar dúvidas — não de ver o conteúdo pela primeira vez. Roteiro previsto:

  1. Revisão relâmpago: por que existem tipos heterogêneos; . × ->; por valor × por ponteiro (retomando a passagem por valor da Semana 04).
  2. Construção ao vivo: modelamos uma entidade do zero — decidir os campos, escolher os tipos, escrever as funções que a manipulam — e montamos um vetor de structs.
  3. Galeria de erros: provocamos de propósito os erros da seção 10 (o ; faltando, . no lugar de ->, a struct que "não muda") para você aprender a ler o compilador.

Problema-guia do encontro — cadastro de produtos com relatório de estoque: modelar um Produto (nome, preço, quantidade), ler n produtos num vetor de structs e emitir um relatório com o valor total de cada item e do estoque inteiro.

estoque.c
#include <stdio.h>

typedef struct {
    char nome[50];
    double preco;
    int quantidade;
} Produto;

// por ponteiro const: só LÊ, e evita copiar a struct
double valor_do_item(const Produto *p) {
    return p->preco * p->quantidade;   // acesso via -> (temos um ponteiro)
}

int main(void) {
    int n;
    Produto estoque[100];

    printf("Quantos produtos? ");
    scanf("%d", &n);

    for (int i = 0; i < n; i++) {
        printf("Produto %d - nome: ", i + 1);
        scanf("%s", estoque[i].nome);
        printf("Produto %d - preco: ", i + 1);
        scanf("%lf", &estoque[i].preco);
        printf("Produto %d - quantidade: ", i + 1);
        scanf("%d", &estoque[i].quantidade);
    }

    double total_geral = 0.0;
    printf("\n--- Relatorio de estoque ---\n");
    for (int i = 0; i < n; i++) {
        double v = valor_do_item(&estoque[i]);   // passa o ENDEREÇO do i-esimo
        printf("%-20s x%3d  R$ %8.2f\n",
               estoque[i].nome, estoque[i].quantidade, v);
        total_geral += v;
    }
    printf("----------------------------\n");
    printf("Valor total do estoque: R$ %.2f\n", total_geral);
    return 0;
}

Perguntas para pensar antes do encontro: por que passamos &estoque[i] (e não estoque[i]) para valor_do_item? O que mudaria se a função recebesse Produto p por valor? Por que const no parâmetro é uma boa ideia aqui?


✍️ Sexta — Estudo dirigido

Reserve este tempo para praticar de verdade. O objetivo não é "ver a resposta", e sim modelar, escrever, errar, corrigir e executar.

Sobre o Beecrowd nesta semana

Structs e enums costumam ser pouco cobrados isoladamente no Beecrowd — os problemas do juiz online focam em entrada/saída, contas e laços. Mesmo assim, você pode (e deve) usar structs por conta própria em problemas de cadastro/ordenação para treinar a modelagem, ainda que o enunciado não exija. A tabela abaixo traz problemas em que modelar com struct deixa a solução mais organizada, mesmo sendo opcional.

Problema Como uma struct ajuda
1005 / 1006 Média 1 / Média 2 agrupar as notas + a média num registro
1015 Distância entre Dois Pontos um Ponto {x, y} deixa o código limpo
1042 / 1043 Ordenar Números / Triângulo agrupar valores relacionados
1133 Rest of Numbers intervalos como registro
1168 LED modelar dígitos (opcional)

O foco da semana, porém, são os desafios do zero abaixo — é neles que a modelagem com struct/enum brilha.

Desafios para escrever do zero

  1. Cadastro de turma (vetor de struct): leia n alunos (nome, matrícula, média) num Aluno turma[]; calcule a média da turma e imprima o melhor aluno (maior média). (Dica: guarde o índice do melhor num inteiro enquanto percorre.)
  2. Ponto e distância: defina typedef struct { double x, y; } Ponto; e uma função double distancia(Ponto a, Ponto b) que calcula a distância euclidiana entre dois pontos (use sqrt de <math.h>; compile com -lm).
  3. Datas: defina Data { int dia, mes, ano; } e escreva int valida(Data d) (o dia/mês fazem sentido?) e int compara(Data a, Data b) (devolve -1, 0 ou 1 conforme a seja anterior, igual ou posterior a b).
  4. Relatório de estoque: parta do Produto do encontro e acrescente: encontrar o produto mais caro e o de maior valor em estoque (preço × quantidade).
  5. Enum em ação: crie enum Naipe { PAUS, OUROS, COPAS, ESPADAS }; (ou um enum de dias) e escreva uma função que, dado um valor do enum, imprime seu nome usando switch.
  6. Structs aninhadas: junte tudo — um Aluno com um campo Data nascimento. Leia e imprima um aluno completo, acessando a.nascimento.ano.

Para investigar (curiosidade prática)

  • Imprima sizeof de uma struct (por exemplo, struct { int a; char b; double c; }) e compare com a soma dos sizeof de cada campo. Bate? Explique a diferença com a ideia de alinhamento/padding da seção 9. Experimente reordenar os campos e veja se o tamanho muda.
  • Pegue uma struct a e um ponteiro Aluno *p = &a;. Acesse o mesmo campo das três formas: a.media, (*p).media e p->media. Confirme que dão o mesmo resultado e explique por que -> é o atalho preferido.

✅ Checklist de autoavaliação

Marque com sinceridade — se algum item não estiver ✅, volte à seção correspondente.

  • Explico por que usar uma struct (dados heterogêneos de uma entidade) e por que ela é melhor que vetores paralelos.
  • Defino uma struct, declaro variáveis e acesso campos com o ponto (.) — sem esquecer o ; final da definição.
  • Sei inicializar structs e simplificar o tipo com typedef.
  • Entendo que passar struct por valor copia todos os campos e não altera o original; sei quando passar por ponteiro.
  • Uso o operador seta (->) com ponteiros e sei que p->campo é (*p).campo.
  • Construo e percorro um vetor de structs (turma[i].media).
  • Uso enum para nomear constantes e o combino com switch, ganhando legibilidade.
  • Sei que sizeof de uma struct pode ser maior que a soma dos campos (padding).

🔗 Referências e para se aprofundar

Antes de seguir para a Semana 15

Você acabou de aprender a modelar uma entidade — a juntar num só "pacote" tudo o que descreve um aluno, um produto, uma data. Guarde esse aluno com carinho: na Semana 15 veremos manipulação de arquivos, e as structs serão a peça central para salvar esses registros em disco e lê-los de volta depois — afinal, de que adianta cadastrar quarenta alunos se tudo se perde quando o programa fecha? Um registro cabe naturalmente numa "linha" de arquivo; é a ponte perfeita entre o que você fez esta semana e o armazenamento permanente dos seus dados.