Tipos Definidos pelo Programador11.1
Nesta aula, nós não vamos estudar struct, typedef, enum e passagem de estruturas para funções como tópicos isolados. Vamos tratar esse conteúdo do jeito como ele costuma aparecer de verdade em programação: quando um programa começa pequeno, parece suficiente, mas logo passa a exigir uma modelagem melhor para continuar crescendo sem virar um conjunto confuso de variáveis e regras espalhadas.
Vamos construir, passo a passo, um pequeno sistema de boletim de alunos. Em cada etapa, a entrada muda um pouco, a regra de negócio fica mais rica e a solução anterior começa a mostrar suas limitações. O que hoje parece só um detalhe de organização rapidamente se transforma em uma exigência real do programa.
Baixe os casos de teste da aula para acompanhar o estudo dirigido.
Como usar o arquivo .zip11.1.1
Depois de baixar e extrair o material, você encontrará uma pasta para cada etapa da aula. Em cada etapa existem duas subpastas:
inputs/, com os arquivos de entradaoutputs/, com as saídas esperadas
O uso recomendado é este:
- leia a etapa correspondente na aula
- implemente ou acompanhe o código daquela etapa
- compile o programa da etapa
- execute usando redirecionamento de entrada
- compare sua saída com o arquivo esperado
Por exemplo:
gcc main.c -o programa
./programa < inputs/case1.in
Se a sua saída não coincidir com o arquivo correspondente em outputs/, isso indica que ainda existe alguma diferença entre a lógica implementada e o comportamento esperado para aquela etapa.
Problema Inicial11.2
Nosso primeiro problema é simples de propósito. O programa vai receber os dados de um único aluno nesta ordem:
nome matricula nota1 nota2 nota3
Por exemplo:
Ana 1001 8.0 7.0 9.0
E, a partir disso, precisamos produzir:
- o nome do aluno
- a matrícula
- a média
- a situação final
Com as aulas de variáveis, operações, condicionais e funções, já conseguimos resolver isso sem dificuldade. O código da primeira etapa usa variáveis simples e duas funções auxiliares. Ou seja, tecnicamente o problema ainda cabe muito bem na base da disciplina já estudada. Trecho central:
float calcular_media(float nota1, float nota2, float nota3){
return (nota1 + nota2 + nota3) / 3.0f;
}
const char *classificar_media(float media){
if(media >= 7.0f)
return "APROVADO";
if(media >= 5.0f)
return "RECUPERACAO";
return "REPROVADO";
}
E no main:
char nome[50];
int matricula;
float nota1, nota2, nota3;
Esas abordagem funciona para uma pessoa e, nesse ponto, seria errado dizer que a solução está incorreta. O problema é outro. Pense com calma no que acontece quando o sistema cresce:
- e se o programa precisar armazenar 30 alunos?
- e se quisermos passar “um aluno” para uma função?
- e se precisarmos guardar as três notas dentro da mesma entidade lógica?
O que quebra aqui não é a lógica do cálculo. A média continua sendo calculada corretamente. O que começa a ficar ruim é a organização dos dados. O programa sabe calcular, mas ainda não sabe representar bem aquilo que está calculando.
Neste ponto, o programa realmente manipula um “aluno” ou apenas cinco variáveis sem ligação explícita entre si?
Transformando as variáveis soltas em uma estrutura única11.3
Agora vamos resolver exatamente o mesmo problema, com a mesma entrada e a mesma ideia geral de saída. A diferença é que, desta vez, vamos explicitar no código algo que antes só existia na nossa interpretação do enunciado: aquelas informações pertencem a uma única entidade lógica, o aluno. Ainda recebemos:
nome matricula nota1 nota2 nota3
Mas agora queremos deixar explícito no código que essas informações pertencem ao mesmo aluno e que devem viajar juntas quando o programa precisar consultá-las, imprimi-las ou passá-las para uma função.
Uma struct permite agrupar, sob um mesmo nome, várias informações que pertencem à mesma entidade. Em vez de tratar nome, matrícula e notas como variáveis independentes, passamos a dizer ao programa que tudo isso forma um único objeto lógico.
Esse é o ponto mais importante da ideia de estrutura em C. Ela não existe para deixar o código mais sofisticado visualmente. Ela existe para fazer a representação do problema ficar mais fiel. Se o problema fala de um aluno, o programa deveria conseguir trabalhar com algo que também se comporte como um aluno.
Em C, a declaração de uma estrutura segue esta forma:
struct NomeDaStruct {
tipo campo1;
tipo campo2;
tipo campo3;
};
Cada campo pode ter um tipo diferente. Isso é justamente o que torna struct tão útil para modelar entidades do mundo real. Um aluno pode ter uma string para o nome, um inteiro para a matrícula e um conjunto de números reais para as notas.
Depois da definição, declaramos variáveis desse tipo assim:
struct NomeDaStruct variavel;
Essa forma funciona bem, mas em C ela costuma ficar um pouco repetitiva, porque obriga o programador a escrever a palavra struct toda vez que quiser declarar uma variável daquele tipo.
É justamente aí que entra o typedef. Ele permite criar um nome de tipo mais curto e mais conveniente para usar no restante do código.
Por exemplo, sem typedef, ficaria assim:
struct Aluno {
char nome[50];
int matricula;
float notas[3];
};
struct Aluno aluno;
Com typedef, podemos escrever assim:
typedef struct {
char nome[50];
int matricula;
float notas[3];
} Aluno;
Aluno aluno;
O papel do typedef, portanto, não é criar uma estrutura nova diferente. Ele apenas cria um nome de tipo mais simples para aquilo que já definimos. No nosso caso usamos nesta etapa:
typedef struct {
char nome[50];
int matricula;
float notas[3];
} Aluno;
Repare que já aproveitamos também a aula de vetores. Em vez de três variáveis nota1, nota2 e nota3, passamos a armazenar as notas em um vetor de tamanho 3 dentro da estrutura. Isso já melhora bastante a modelagem.
Como acessar os campos11.3.0.1
Quando temos uma variável comum do tipo estrutura, usamos o operador . para acessar seus campos.
Aluno aluno;
printf("%s\n", aluno.nome);
printf("%d\n", aluno.matricula);
printf("%.2f\n", aluno.notas[0]);
Se o campo da estrutura for uma string, continuamos obedecendo às mesmas regras da aula de strings. Isso significa que copiar texto para dentro do campo ainda exige strcpy(), porque arrays de caracteres não aceitam atribuição direta com =.
Quando a estrutura aparece como uma variável comum, a leitura é sempre esta:
aluno.nomealuno.matriculaaluno.notas[0]
Em vários trechos desta aula, a estrutura não será recebida como uma variável comum, mas como um ponteiro para estrutura. Nesses casos, o acesso aos campos deixa de usar . e passa a usar ->.
Por exemplo:
Aluno aluno;
Aluno *p = &aluno;
printf("%s\n", p->nome);
printf("%d\n", p->matricula);
printf("%.2f\n", p->notas[0]);
Por enquanto, a ideia mais importante aqui é apenas esta:
aluno.nomeacessa um campo quando temos a estrutura diretamentep->nomeacessa um campo quando temos um ponteiro para a estrutura
Nesta aula, você pode ler -> apenas como a forma de acessar campos de uma estrutura recebida por referência. A explicação completa de por que isso funciona, e qual é a relação entre &, * e ->, ficará para a próxima aula, dedicada especificamente a ponteiros.
O que o código ganha com isso11.3.1
Antes, o programa recebia “peças soltas”. Agora ele recebe um Aluno.
A função da média deixa de operar sobre três notas separadas e passa a operar sobre uma estrutura. Como ela recebe esse aluno por referência, o acesso aos campos aparece com -> em vez de .:
float aluno_media(const Aluno *a){
return (a->notas[0] + a->notas[1] + a->notas[2]) / 3.0f;
}
Por enquanto, trate isso como uma leitura guiada: a representa o aluno recebido pela função, e a->notas[0] significa que estamos acessando a primeira nota desse aluno. Na próxima aula, vamos desmontar essa sintaxe com calma para que ela deixe de parecer apenas um detalhe decorado.
Mesmo com um único aluno, isso já mostra uma vantagem importante: os dados ficam agrupados e a função começa a trabalhar com um objeto mais coerente.
Se amanhã o aluno ganhar mais um campo, como curso ou periodo, o código com struct cresce de forma mais organizada do que o código com variáveis soltas?
Agora não é mais um aluno, é uma turma11.4
Até agora, mesmo com struct, nosso programa ainda resolve um problema pequeno. A próxima mudança natural é esta: em vez de um aluno, vamos receber uma turma inteira.
Agora a entrada começa com um inteiro n, indicando a quantidade de alunos. Depois disso, vêm os dados dos n alunos:
n
nome matricula nota1 nota2 nota3
nome matricula nota1 nota2 nota3
...
Exemplo:
3
Ana 1001 8.0 7.0 9.0
Bruno 1002 5.0 6.0 4.0
Caio 1003 3.0 4.0 5.0
O que precisamos fazer agora11.4.1
Além de calcular a média individual, precisamos:
- imprimir um relatório da turma
- contar quantos alunos foram aprovados
- contar quantos ficaram em recuperação
- contar quantos foram reprovados
- identificar a melhor média da turma
Como Aluno agora é um tipo, podemos criar um vetor desse tipo:
Aluno turma[100];
Ou seja, estamos aplicando a ideia da aula de arrays a um tipo definido pelo programador. Isso é importante porque mostra que struct não substitui o que você aprendeu sobre vetores. Na verdade, ela se integra a esse conteúdo e torna os vetores mais expressivos.
Essa etapa é importante porque une três ideias que antes apareciam separadas:
- vetores
- laços de repetição
- estruturas
Observe a lógica principal:
for(i = 0; i < n; i++){
ler_aluno(&turma[i]);
}
for(i = 0; i < n; i++){
float media = aluno_media(&turma[i]);
const char *situacao = classificar_media(media);
...
}
O ganho aqui é enorme. Em vez de duplicar lógica para cada aluno, tratamos todos os registros da mesma forma e deixamos que o índice do vetor indique com qual aluno estamos trabalhando em cada instante. A repetição continua a mesma, mas o significado dos dados ficou muito mais forte.
Até este ponto, a situação do aluno ainda é representada por texto comum. Na prática, a lógica vinha produzindo strings como "APROVADO", "RECUPERACAO" e "REPROVADO", como já acontecia nas etapas anteriores com a função classificar_media().
Isso funciona enquanto a regra é pequena e a situação serve apenas para ser impressa. O problema começa quando esse estado passa a participar mais da lógica do programa.
A situação do aluno vira um conjunto formal de estados11.5
Nosso sistema ficou melhor, mas ainda existe uma fragilidade conceitual. Até aqui, a situação do aluno era basicamente um texto devolvido por função, algo como:
const char *classificar_media(float media){
if(media >= 7.0f)
return "APROVADO";
if(media >= 5.0f)
return "RECUPERACAO";
return "REPROVADO";
}
Essa abordagem é suficiente quando queremos apenas exibir uma mensagem. Mas, quando a situação vira parte da regra do sistema, depender apenas de texto começa a introduzir ruído na modelagem.
Quando fazemos isso, fica fácil cometer erros, como:
- escrever uma string diferente do esperado
- repetir regras em vários pontos
- usar números mágicos sem significado claro
Agora cada aluno também terá o número de faltas:
n
nome matricula nota1 nota2 nota3 faltas
...
Agora a situação do aluno depende de duas coisas:
- média
- faltas
Antes de seguir, vale nomear uma ideia nova. Uma enum é um tipo usado para representar um conjunto pequeno e fixo de estados nomeados. Em vez de guardar a situação do aluno como texto solto ou como números sem significado claro, podemos trabalhar com nomes como APROVADO, RECUPERACAO e REPROVADO_FALTA.
Em C, esses nomes funcionam como constantes inteiras organizadas sob o mesmo tipo lógico. O importante, nesta etapa, não é decorar a sintaxe imediatamente, mas perceber a vantagem de modelagem: o programa deixa de lidar com textos arbitrários e passa a lidar com estados formais.
As regras ficam assim:
- se
faltas > 18, então o aluno está emREPROVADO_FALTA - caso contrário, se
media >= 7.0, entãoAPROVADO - caso contrário, se
media >= 5.0, entãoRECUPERACAO - caso contrário,
REPROVADO_NOTA
Agora faz sentido criar uma enumeração para representar formalmente esses estados. O ganho aqui não é apenas estético. Em vez de usar texto comum para representar a situação internamente, o programa passa a trabalhar com categorias explícitas. A string continua existindo, mas apenas na hora de imprimir o resultado para o usuário.
Vamos reaproveitar a mesma ideia de typedef usada com struct: em vez de escrever enum SituacaoAluno toda vez, criamos um nome de tipo mais simples para a enumeração.
typedef enum {
REPROVADO_NOTA,
RECUPERACAO,
APROVADO,
REPROVADO_FALTA
} SituacaoAluno;
Isso nos permite separar duas coisas que antes estavam misturadas:
- a regra lógica da situação
- a forma textual como essa situação será impressa
Por exemplo:
SituacaoAluno aluno_situacao(const Aluno *a){
float media = aluno_media(a);
if(a->faltas > 18)
return REPROVADO_FALTA;
if(media >= 7.0f)
return APROVADO;
if(media >= 5.0f)
return RECUPERACAO;
return REPROVADO_NOTA;
}
E depois, em outra função:
const char *situacao_texto(SituacaoAluno situacao){
switch(situacao){
case APROVADO: return "APROVADO";
case RECUPERACAO: return "RECUPERACAO";
case REPROVADO_NOTA: return "REPROVADO_NOTA";
case REPROVADO_FALTA: return "REPROVADO_FALTA";
}
return "DESCONHECIDA";
}
Agora o programa ficou mais rico logicamente. Não estamos apenas organizando dados. Estamos também organizando estados. Essa separação melhora muito a clareza do código quando a regra de negócio começa a crescer, porque fica mais fácil distinguir o cálculo da média, a decisão sobre a situação e a forma como essa situação será exibida.
A estrutura do programa agora precisa acompanhar a estrutura dos dados11.6
Até aqui, o código cresceu bastante. E agora aparece um problema novo: tudo continua dentro de um único arquivo. Isso não atrapalha apenas a leitura. Também torna mais difícil enxergar onde termina a definição dos tipos, onde começa a interface pública do módulo e onde fica a implementação real das funções.
Isso começa a conflitar diretamente com a aula de bibliotecas. Se queremos um programa mais organizado, precisamos separar:
- a definição dos tipos
- os protótipos das funções
- a implementação das funções
- o programa principal
Vamos manter a mesma entrada e a mesma lógica da etapa 4. A diferença agora é organizacional. O comportamento geral do programa quase não muda, mas a forma de distribuir as responsabilidades no projeto passa a acompanhar melhor o que foi estudado na aula de bibliotecas.
Nesta etapa, o código é dividido em:
aluno.htypedef structtypedef enum- protótipos das funções
- header guard
aluno.c- as implementações das funções
main.c- a leitura da entrada
- a chamada das funções da biblioteca
- a impressão do relatório
O programa faz praticamente a mesma coisa da etapa anterior, mas a organização interna melhorou muito. Esse é um aprendizado importante: às vezes a evolução do código não está em “fazer uma conta nova”, mas em separar responsabilidades. Quando tipos, protótipos e implementações deixam de ficar todos misturados, o programa fica mais fácil de manter, de reaproveitar e de ampliar.
Compilação11.6.1
Agora a compilação muda, exatamente como foi visto na aula de bibliotecas:
gcc main.c aluno.c -o programa
Consultar não basta, agora precisamos alterar o aluno correto11.7
Até aqui, nossas funções praticamente consultavam dados e geravam relatórios. Agora o programa vai crescer mais uma vez e isso muda a natureza do problema. Não basta mais observar a turma. Vamos precisar localizar um aluno específico e alterar o seu conteúdo original dentro do vetor.
Além da turma, a entrada agora vai trazer uma lista de operações de bônus:
n
nome matricula nota1 nota2 nota3 faltas
...
m
matricula bonus
matricula bonus
...
Depois de ler a turma, o programa deve aplicar cada bônus ao aluno correspondente.
O bônus não será somado às três notas. Ele será aplicado apenas à menor nota do aluno. Se a nota ultrapassar 10.0, ela deve ser limitada em 10.0. Isso deixa a lógica mais interessante porque agora precisamos:
- buscar um aluno por matrícula dentro do vetor
- devolver o endereço desse aluno
- alterar diretamente a estrutura encontrada
Agora faz todo sentido usar funções como estas:
Aluno *buscar_aluno_por_matricula(Aluno turma[], int n, int matricula);
void aplicar_bonus(Aluno *a, float bonus);
Na primeira função, retornamos um ponteiro para o aluno encontrado. Se a matrícula não existir no vetor, a função pode retornar NULL, que em C indica a ausência de um endereço válido.
Na segunda, recebemos um ponteiro para o aluno e alteramos diretamente seu conteúdo. O acesso aos campos é feito com ->:
a->notas[indice_menor] += bonus;
Antes, o sistema apenas observava os alunos. Agora ele passa a modificar os alunos corretos dentro da turma. É justamente aqui que ponteiros para estruturas deixam de parecer um detalhe de sintaxe e passam a ter uma função concreta no projeto.
É aqui que a diferença entre passar uma estrutura por valor e por referência fica realmente concreta.
Se aplicar_bonus recebesse um Aluno por valor, a alteração feita dentro da função chegaria até o vetor da turma?
Agora o aluno carrega outra estrutura dentro dele11.8
Nosso sistema já trabalha com tipo definido pelo programador, vetor, enumeração, biblioteca e alteração por referência. Vamos acrescentar mais uma camada de modelagem. Isso é importante porque, em sistemas reais, as entidades raramente são planas. Um aluno pode carregar outras informações estruturadas dentro dele, como cidade, estado, endereço ou dados de contato.
Agora cada aluno também terá cidade e estado:
n
nome matricula nota1 nota2 nota3 faltas cidade estado
...
m
matricula bonus
...
estado_consulta
No fim da entrada, o programa recebe um estado e precisa listar apenas os alunos daquele estado.
Vamos criar uma estrutura para a localização:
typedef struct {
char cidade[30];
char estado[3];
} Localidade;
E incorporá-la à estrutura do aluno:
typedef struct {
char nome[50];
int matricula;
float notas[3];
int faltas;
Localidade local;
} Aluno;
Agora o acesso passa a usar dois níveis lógicos:
aluno.local.cidade
aluno.local.estado
ou, se estivermos com ponteiro:
a->local.estado
Além de tudo que já fazia antes, ele agora também deve:
- imprimir a cidade e o estado no relatório
- filtrar alunos por estado
- contar quantos alunos daquele estado foram aprovados
Perceba como o programa foi crescendo sem abandonar a base anterior. Nós só fomos acrescentando camadas mais organizadas de modelagem e de lógica. Essa é justamente a ideia central deste estudo dirigido: mostrar que a solução não precisa ser recomeçada do zero sempre que o problema evolui, desde que ela tenha sido bem organizada.
Questões11.9
1. Na etapa 1, o programa já funciona. Então por que ainda dizemos que a modelagem é ruim?
2. Reescreva a solução da etapa 1 usando uma struct Aluno, mas sem usar vetor de notas. Depois compare essa versão com a da etapa 2 e diga qual ficou mais organizada.
3. Na etapa 3, acrescente ao relatório a menor média da turma, sem remover nenhuma função já existente.
4. Na etapa 4, modifique as regras para que alunos com média maior ou igual a 9.0 recebam a observação textual DESTAQUE, sem criar um novo valor no enum.
5. Na etapa 5, crie uma nova função na biblioteca chamada int aluno_foi_aprovado(const Aluno *a) e use essa função no main para contar aprovados.
6. Na etapa 6, altere a função de bônus para aplicar o valor na maior nota, e não na menor. Depois compare o efeito dessa mudança nas saídas.
7. Na etapa 6, faça com que o programa informe também quantas operações de bônus falharam porque a matrícula não foi encontrada.
8. Na etapa 7, além de consultar por estado, faça uma consulta por cidade. A entrada deve receber mais uma string ao final, e o programa deve imprimir os alunos daquela cidade.
9. Na etapa 7, acrescente ao relatório final a quantidade de aprovados por estado, sem remover a funcionalidade anterior de listagem.
10. Explique com suas palavras por que a.local.estado e a->local.estado não são a mesma coisa em termos de sintaxe, embora apontem para o mesmo campo lógico do aluno.
1. Porque os dados do aluno ainda estão espalhados em variáveis independentes, o que dificulta crescimento, manutenção e passagem para funções.
2. A resposta deve mostrar uma struct com três campos de nota separados e depois comparar com a versão que usa float notas[3]. A versão com vetor tende a escalar melhor para operações com laço.
3. Espera-se uma nova lógica de busca da menor média usando laço e comparação, sem quebrar as funções já existentes.
4. Espera-se uso de if adicional na impressão ou em função de relatório, sem alterar a enumeração principal.
5. A função pode retornar 1 quando aluno_situacao(a) == APROVADO e 0 caso contrário.
6. Espera-se alteração apenas na lógica que escolhe o índice da nota a ser modificada, mantendo a organização geral do programa.
7. Basta manter um contador de falhas e incrementá-lo sempre que buscar_aluno_por_matricula retornar NULL.
8. Espera-se leitura de uma nova string, uso de strcmp e filtragem semelhante à consulta por estado.
9. A solução deve percorrer o vetor e acumular quantidades por estado conforme a regra definida pelo aluno.
10. a.local.estado é acesso direto sobre uma variável do tipo estrutura. a->local.estado é acesso indireto, feito a partir de um ponteiro para estrutura.
Próximos passos11.10
Ao longo desta aula, apareceram chamadas com &, parâmetros com * e acessos com ->. Eles já foram usados aqui porque ajudam o programa a localizar e modificar estruturas reais, mas a lógica por trás disso ainda merece uma aula própria.
Na próxima aula, Ponteiros e Alocação Dinâmica, vamos estudar ponteiros e alocação dinâmica: o que é um endereço de memória, o que um ponteiro realmente armazena, por que & e * aparecem em funções, como o operador -> se conecta com estruturas e, em seguida, como criar e redimensionar dados durante a execução com malloc e free.