Aula 10 - Estruturas (struct)
Até agora, cada dado do nosso programa vivia em uma variável separada: um int para a idade, um char[] para o nome, um float para a altura. Nesta aula, veremos como agrupar dados relacionados sob um único tipo, criado por nós mesmos: a struct (estrutura).
1. Motivação: por que agrupar dados relacionados?
Imagine um programa que precisa guardar informações de uma pessoa: nome, idade e altura. Sem structs, isso seria feito com três variáveis soltas:
char nome[100]; int idade; float altura;
Isso já é incômodo para uma pessoa. Agora imagine um programa que precisa lidar com uma turma inteira de alunos, cada um com nome, idade e nota — precisaríamos de três vetores paralelos (nomes[50], idades[50], notas[50]), todos indexados pelo mesmo i, e a responsabilidade de nunca deixá-los "dessincronizar" ficaria inteiramente com o programador. Passar "um aluno" para uma função exigiria passar três parâmetros separados, sempre na ordem certa.
O problema, em uma frase. Variáveis que descrevem o mesmo conceito (uma pessoa, um aluno, um ponto no plano) ficam soltas, sem nenhuma relação explícita no código — cabe inteiramente ao programador lembrar que nomes[i], idades[i] e notas[i] "pertencem" ao mesmo aluno.
|
Uma struct resolve isso agrupando variáveis de tipos diferentes sob um único nome. E aqui está o ponto mais importante desta aula, que vale destacar antes de qualquer sintaxe:
Uma struct define um tipo de dado, não uma variável. Assim como int, float e char são tipos que descrevem "a forma" de um dado (quantos bytes ocupa, como interpretá-lo) mas não são, eles próprios, nenhum valor específico, uma struct também descreve apenas a forma de um dado composto — quais membros ele tem e de que tipo é cada um. Ao escrever typedef struct {...} Aluno;, ainda não existe nenhum aluno em memória: criamos apenas a "planta", o tipo Aluno. Uma variável só passa a existir quando declaramos algo como Aluno a1; — exatamente como int x; declara uma variável do tipo int.
|
Isso encaixa a struct dentro de um panorama mais amplo de tipos de dados que já conhecemos:
| Categoria | Exemplos | Característica |
|---|---|---|
| Tipos primitivos | int, float, char |
Guardam um único valor "atômico" por vez — não têm partes internas nomeadas. |
| Tipos estruturados | Vetores e matrizes (Aulas 4 e 5); struct (esta aula) | Agrupam vários valores sob um único nome, formando um dado composto. |
A diferença entre os dois tipos estruturados que já conhecemos está em como os valores são agrupados e acessados: um vetor agrupa vários valores do mesmo tipo, acessados por índice (v[0], v[1], ...); uma struct agrupa valores que podem ser de tipos diferentes, acessados por nome (a.nome, a.idade, ...).
Atenção: essa semelhança com vetores é só superficial — o momento em que cada um "existe" é bem diferente. Quando você escreve int v[5];, essa linha já cria uma variável: 5 espaços de memória para inteiros são reservados naquele exato momento. Vetor é sempre uma variável (ou parte de uma), nunca um passo separado de "definir o tipo" — não existe uma etapa prévia de "declarar o tipo vetor de 5 inteiros" separada de criar a variável propriamente dita.
Uma struct funciona de um jeito fundamentalmente diferente, em duas etapas separadas:
Aluno — e normalmente é escrita uma única vez, fora de qualquer função, para todo o programa. Já cada Aluno a1;, Aluno a2; (passo 2) cria uma variável nova e independente, seguindo aquela mesma planta.
|
Isso facilita a leitura do código, a passagem para funções (um único parâmetro em vez de vários) e o armazenamento em vetores (um vetor do novo tipo, em vez de vários vetores paralelos).
2. Declarando e usando uma struct
Em C, a forma mais básica de declarar uma struct usa a palavra-chave struct seguida de um nome (a chamada tag da struct) e a lista de membros entre chaves:
#include <stdio.h> #include <string.h> // Define o tipo "struct Pessoa" (fora de qualquer função) struct Pessoa { char nome[100]; int idade; float altura; }; int main() { // Toda vez que declaramos uma variável, é preciso repetir "struct" struct Pessoa pessoa1; strcpy(pessoa1.nome, "Joao"); pessoa1.idade = 30; pessoa1.altura = 1.80; printf("Nome: %s, Idade: %d\n", pessoa1.nome, pessoa1.idade); return 0; }
Cada campo dentro da struct é chamado de membro (ou campo), e é acessado através do operador ponto (.): pessoa1.nome, pessoa1.idade, pessoa1.altura.
Vale a pena ver exatamente como isso fica na memória, usando a mesma tabela Endereço/ID/Tipo/Valor das Aulas 1 e 9. Ao contrário de uma variável simples, uma struct Pessoa ocupa uma região contígua de memória, grande o suficiente para caber todos os seus membros um atrás do outro, na ordem em que foram declarados:
| Endereço | ID | Tipo | Valor |
|---|---|---|---|
E1000 |
pessoa1.nome |
char[100] |
"Joao\0..." |
E1100 |
pessoa1.idade |
int |
30 |
E1104 |
pessoa1.altura |
float |
1.80 |
Repare que pessoa1 não é "uma linha" da tabela — ela é a região inteira, de E1000 até o fim de altura: o próprio endereço de pessoa1 (&pessoa1) é E1000, o mesmo endereço do seu primeiro membro. Cada membro, dentro dessa região, é uma "sub-variável" com seu próprio endereço, tipo e valor, exatamente como qualquer variável comum — a diferença é que o endereço de cada membro é calculado a partir do início da struct, somando o tamanho (em bytes) de todos os membros anteriores.
Nesse exemplo, nome ocupa 100 bytes; por isso idade começa em E1000 + 100 = E1100. Em seguida, como um int ocupa 4 bytes, altura começa em E1100 + 4 = E1104. Usando a notação decimal E, essa conta é direta — não é preciso converter nada para hexadecimal antes de somar.
Repare que, sem nenhum recurso adicional, o nome do tipo é struct Pessoa — duas palavras, não apenas Pessoa — e essa repetição de struct é obrigatória toda vez que quisermos declarar uma nova variável desse tipo, passar como parâmetro de função, etc. Isso é verboso e, em programas maiores, prejudica a legibilidade.
Introduzindo o typedef
O typedef permite criar um apelido para um tipo já existente — no nosso caso, um apelido mais curto para struct Pessoa, eliminando a necessidade de repetir a palavra struct. É comum (e é o que faremos daqui em diante) declarar a struct e criar esse apelido em um único bloco, deixando a struct sem nome (anônima) e nomeando apenas o apelido:
Sintaxe genérica:
typedef struct {
tipo1 membro1;
tipo2 membro2;
...
tipoN membroN;
} NomeDoTipo;
Reescrevendo o mesmo exemplo, agora com typedef:
#include <stdio.h> #include <string.h> // Define o tipo "Pessoa" diretamente, sem precisar da palavra "struct" depois typedef struct { char nome[100]; int idade; float altura; } Pessoa; int main() { Pessoa pessoa1; // declara normalmente, sem escrever "struct" strcpy(pessoa1.nome, "Joao"); // acessa um membro com "." pessoa1.idade = 30; pessoa1.altura = 1.80; printf("Nome: %s, Idade: %d\n", pessoa1.nome, pessoa1.idade); return 0; }
Uso de typedef é obrigatório nesta disciplina. A partir de agora, toda struct desta disciplina deve ser declarada com typedef, exatamente como no segundo exemplo. A forma sem typedef foi mostrada apenas para você reconhecer o padrão (é comum encontrá-lo em código de terceiros, ou até em provas antigas) e entender exatamente qual problema o typedef resolve — mas todo código que você escrever daqui em diante deve usar typedef, pois isso deixa o código consideravelmente mais legível.
|
Também é possível inicializar todos os membros de uma vez, na própria declaração, na ordem em que foram definidos na struct:
#include <stdio.h> typedef struct { int x; int y; } Ponto; int main() { Ponto p1 = {5, 10}; // p1.x = 5, p1.y = 10 printf("(%d, %d)\n", p1.x, p1.y); // (5, 10) return 0; }
3. Structs e funções
Assim como qualquer outro tipo, uma struct pode ser passada como parâmetro para uma função e devolvida como retorno. E, como vimos nas Aulas 8 e 9, isso pode ser feito de duas formas: por valor (a função recebe uma cópia inteira da struct) ou por referência (a função recebe um ponteiro para a struct original, podendo alterá-la de verdade).
#include <stdio.h> typedef struct { int x; int y; } Ponto; // Protótipos void imprimirPonto(Ponto p); // por valor: só lê void incrementarPonto(Ponto *p); // por referência: altera o original int main() { Ponto p1 = {5, 10}; imprimirPonto(p1); // Ponto: (5, 10) incrementarPonto(&p1); imprimirPonto(p1); // Ponto: (6, 11) -> p1 realmente mudou return 0; } // Implementações void imprimirPonto(Ponto p) { printf("Ponto: (%d, %d)\n", p.x, p.y); } void incrementarPonto(Ponto *p) { p->x++; // operador ->: acessa um membro através de um PONTEIRO para struct p->y++; }
Por que existe o operador ->? Quando temos um ponteiro para uma struct (como Ponto *p), não podemos escrever p.x — p não é uma struct, é um endereço. O acesso correto seria desreferenciar primeiro, com (*p).x (os parênteses são obrigatórios: o operador . tem prioridade sobre o *, então *p.x, sem parênteses, seria interpretado como *(p.x) — um erro de compilação, já que p não tem membro nenhum antes de ser desreferenciado). Para evitar essa armadilha e deixar o código mais legível, C oferece o operador ->, de forma que p->x é exatamente equivalente a (*p).x, mas muito mais direto de escrever e de ler.
|
| Situação | Como acessar o membro x |
|---|---|
Variável comum: Ponto p; |
p.x |
Ponteiro: Ponto *p; |
p->x (equivalente a (*p).x) |
4. Vetores de structs
Um vetor de structs permite armazenar várias instâncias do mesmo tipo de forma organizada — resolvendo, de vez, o problema dos "vetores paralelos" apresentado na motivação desta aula:
#include <stdio.h> #include <string.h> typedef struct { char nome[50]; int idade; } Aluno; int main() { Aluno turma[3]; int i; strcpy(turma[0].nome, "Alice"); turma[0].idade = 20; strcpy(turma[1].nome, "Bob"); turma[1].idade = 21; strcpy(turma[2].nome, "Carla"); turma[2].idade = 19; for (i = 0; i < 3; i++) { printf("%s tem %d anos\n", turma[i].nome, turma[i].idade); } return 0; }
Repare como turma[i] já resolve, sozinho, o problema de manter nome e idade sincronizados: não existe mais risco de "misturar" os dados de dois alunos diferentes, porque cada posição do vetor guarda a struct Aluno completa.
5. Structs aninhadas
Uma struct pode conter outra struct como membro, permitindo organizar dados em camadas — por exemplo, separar a data de nascimento de um contato em seu próprio tipo:
#include <stdio.h> #include <string.h> typedef struct { int dia; int mes; int ano; } Data; typedef struct { char nome[100]; Data nascimento; // struct dentro de struct } Contato; int main() { Contato c; strcpy(c.nome, "Maria"); c.nascimento.dia = 15; // acesso encadeado: c.nascimento (Data), depois .dia c.nascimento.mes = 6; c.nascimento.ano = 1995; printf("%s nasceu em %d/%d/%d\n", c.nome, c.nascimento.dia, c.nascimento.mes, c.nascimento.ano); return 0; }
6. Cópia e comparação de structs
Diferente de vetores, uma struct pode ser copiada inteira com uma simples atribuição (=) — isso copia, de uma vez, o valor de todos os membros:
#include <stdio.h> typedef struct { int x; int y; } Ponto; int main() { Ponto p1 = {10, 20}; Ponto p2 = p1; // cópia: p2 recebe os valores de p1 (x e y) p2.x = 30; printf("p1: (%d, %d)\n", p1.x, p1.y); // ainda (10, 20) printf("p2: (%d, %d)\n", p2.x, p2.y); // agora (30, 20) return 0; }
Já a comparação não pode ser feita diretamente com == — o operador não sabe comparar structs membro a membro. É preciso escrever uma função própria para isso:
#include <stdio.h> typedef struct { int x; int y; } Ponto; // Protótipo int pontosIguais(Ponto p1, Ponto p2); int main() { Ponto p1 = {5, 10}; Ponto p2 = {5, 10}; Ponto p3 = {3, 7}; if (pontosIguais(p1, p2)) { printf("p1 e p2 sao iguais\n"); } if (!pontosIguais(p1, p3)) { printf("p1 e p3 sao diferentes\n"); } return 0; } // Implementação int pontosIguais(Ponto p1, Ponto p2) { return (p1.x == p2.x) && (p1.y == p2.y); }
Boas práticas com structs.
|
Resumo
|
Exercícios
Nível fácil
-
Defina, com
typedef, uma structLivrocom os membrostitulo(char[100]),autor(char[100]) eano(int). Namain(), declare uma variável desse tipo, preencha seus membros e imprima-os. -
Defina uma struct
Retangulocom membroslarguraealtura(ambosfloat). Escreva uma funçãofloat area(Retangulo r)que calcule e retorne a área do retângulo. -
Declare um vetor de 3 structs
Ponto(com membrosxey, inteiros), inicialize cada uma com valores diferentes, e imprima todas em um laçofor.
Nível médio
-
Defina uma struct
Aluno(nome, idade, nota). Escreva uma funçãovoid aumentarNota(Aluno *a, float pontos)que receba um ponteiro para umAlunoe somepontosà sua nota, alterando o aluno original. Use o operador->na implementação. -
Escreva uma função
Aluno melhorAluno(Aluno turma[], int tamanho)que percorra um vetor deAlunoe retorne (por valor) o aluno com a maior nota. -
Explique, sem executar o código, qual será a saída do programa abaixo, e por quê:
#include <stdio.h> typedef struct { int valor; } Caixa; void tentarAlterar(Caixa c) { c.valor = 100; } int main() { Caixa minhaCaixa = {5}; tentarAlterar(minhaCaixa); printf("%d\n", minhaCaixa.valor); return 0; }
Nível difícil
-
Defina uma struct
Data(dia, mes, ano) e uma structEventoque contenha umchar nome[50]e umaData data(struct aninhada). Escreva uma funçãoint depoisDe(Data d1, Data d2)que retorne1sed1for uma data posterior ad2, e0caso contrário (compare primeiro o ano, depois o mês, depois o dia). Utilize-a para ordenar (ou apenas identificar o mais recente de) um pequeno vetor deEvento. -
O programa abaixo tenta comparar duas structs diretamente com
==, o que não compila em C. Explique por que essa comparação não é permitida, e reescreva o trecho usando uma função de comparação membro a membro:#include <stdio.h> typedef struct { int x; int y; } Ponto; int main() { Ponto a = {1, 2}; Ponto b = {1, 2}; if (a == b) { // ERRO de compilação printf("Iguais\n"); } return 0; }
Sugestões de Respostas dos Exercícios
Exercício 1
#include <stdio.h> #include <string.h> typedef struct { char titulo[100]; char autor[100]; int ano; } Livro; int main() { Livro l; strcpy(l.titulo, "O Senhor dos Aneis"); strcpy(l.autor, "J.R.R. Tolkien"); l.ano = 1954; printf("%s, de %s (%d)\n", l.titulo, l.autor, l.ano); return 0; }
Exercício 2
#include <stdio.h> typedef struct { float largura; float altura; } Retangulo; float area(Retangulo r); int main() { Retangulo r = {4.0, 5.0}; printf("Area: %.2f\n", area(r)); return 0; } float area(Retangulo r) { return r.largura * r.altura; }
Exercício 3
#include <stdio.h> typedef struct { int x; int y; } Ponto; int main() { Ponto pontos[3] = {{1, 2}, {3, 4}, {5, 6}}; int i; for (i = 0; i < 3; i++) { printf("(%d, %d)\n", pontos[i].x, pontos[i].y); } return 0; }
Exercício 4
#include <stdio.h> #include <string.h> typedef struct { char nome[50]; int idade; float nota; } Aluno; void aumentarNota(Aluno *a, float pontos); int main() { Aluno a; strcpy(a.nome, "Pedro"); a.idade = 20; a.nota = 6.5; printf("Antes: %.1f\n", a.nota); aumentarNota(&a, 1.5); printf("Depois: %.1f\n", a.nota); return 0; } void aumentarNota(Aluno *a, float pontos) { a->nota = a->nota + pontos; }
Exercício 5
#include <stdio.h> #include <string.h> typedef struct { char nome[50]; float nota; } Aluno; Aluno melhorAluno(Aluno turma[], int tamanho); int main() { Aluno turma[3]; Aluno melhor; strcpy(turma[0].nome, "Ana"); turma[0].nota = 8.0; strcpy(turma[1].nome, "Bruno"); turma[1].nota = 9.5; strcpy(turma[2].nome, "Carla"); turma[2].nota = 7.0; melhor = melhorAluno(turma, 3); printf("Melhor aluno: %s (%.1f)\n", melhor.nome, melhor.nota); return 0; } Aluno melhorAluno(Aluno turma[], int tamanho) { int i; Aluno melhor = turma[0]; for (i = 1; i < tamanho; i++) { if (turma[i].nota > melhor.nota) { melhor = turma[i]; // cópia da struct inteira } } return melhor; // retorna a struct por valor (uma cópia) }
Exercício 6
A saída será 5 (o valor original, não 100). A função tentarAlterar recebe c por valor — ou seja, c é uma cópia completa da struct minhaCaixa. A instrução c.valor = 100; altera apenas essa cópia local, que deixa de existir assim que tentarAlterar termina; a struct original, minhaCaixa, na main(), nunca é tocada. Isso é exatamente o mesmo comportamento de passagem por valor que já vimos para tipos simples (Aula 8) — structs não são exceção.
Exercício 7
#include <stdio.h> #include <string.h> typedef struct { int dia; int mes; int ano; } Data; typedef struct { char nome[50]; Data data; } Evento; int depoisDe(Data d1, Data d2); int main() { Evento eventos[3]; int i; int indiceMaisRecente = 0; strcpy(eventos[0].nome, "Reuniao"); eventos[0].data = (Data) {10, 3, 2024}; strcpy(eventos[1].nome, "Entrega"); eventos[1].data = (Data) {22, 11, 2024}; strcpy(eventos[2].nome, "Prova"); eventos[2].data = (Data) {5, 6, 2024}; for (i = 1; i < 3; i++) { if (depoisDe(eventos[i].data, eventos[indiceMaisRecente].data)) { indiceMaisRecente = i; } } printf("Evento mais recente: %s\n", eventos[indiceMaisRecente].nome); return 0; } int depoisDe(Data d1, Data d2) { if (d1.ano != d2.ano) { return d1.ano > d2.ano; } if (d1.mes != d2.mes) { return d1.mes > d2.mes; } return d1.dia > d2.dia; }
Exercício 8
O operador == em C compara apenas tipos escalares (inteiros, ponto flutuante, ponteiros) byte a byte de forma direta. Ele não sabe, de forma automática, como comparar uma estrutura composta por vários membros — o compilador não gera esse código sozinho, então a == b nem chega a compilar quando a e b são structs. É por isso que sempre escrevemos uma função de comparação própria, testando cada membro individualmente:
#include <stdio.h> typedef struct { int x; int y; } Ponto; int pontosIguais(Ponto p1, Ponto p2); int main() { Ponto a = {1, 2}; Ponto b = {1, 2}; if (pontosIguais(a, b)) { printf("Iguais\n"); } return 0; } int pontosIguais(Ponto p1, Ponto p2) { return (p1.x == p2.x) && (p1.y == p2.y); }