Reforço de Algoritmos e Programação - Instituto de Informática (UFRGS) - Prof. Dennis Giovani Balreira



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 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:
  1. Escrever typedef struct {...} Aluno; não cria nenhuma variável, e não reserva memória nenhuma. Isso só define o conceito — a "planta", o blueprint — de o que é um Aluno: quais membros ele tem, e de que tipo é cada um.
  2. Só quando escrevemos Aluno a1; essa planta é, de fato, implementada: uma variável real é criada, e só então a memória é reservada.
Ou seja: a definição da struct (passo 1) pode existir no programa sem que exista nenhuma variável do tipo 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.xp 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.
  • Use typedef: simplifica a sintaxe e torna o código mais legível.
  • Prefira passar por referência (ponteiro) para structs grandes: evita copiar todos os membros a cada chamada de função — use por valor apenas quando a função realmente não deve alterar o original, e a struct for pequena.
  • Use o operador -> com ponteiros para struct: é equivalente a (*p).membro, porém mais legível.
  • Compare manualmente: escreva uma função de comparação membro a membro, em vez de tentar usar == diretamente.

Resumo

  • Struct: agrupa variáveis de tipos diferentes sob um único nome, criando um novo tipo de dado. Declarada com typedef struct { ... } NomeDoTipo;.
  • Membros são acessados com . (variável comum) ou -> (através de um ponteiro para a struct) — p->x é equivalente a (*p).x.
  • Structs podem ser passadas para funções por valor (cópia) ou por referência (ponteiro, permitindo alterar o original), exatamente como qualquer outro tipo.
  • Vetores de structs agrupam várias instâncias do mesmo tipo, resolvendo o problema de "vetores paralelos".
  • Structs podem ser aninhadas (uma struct como membro de outra) e copiadas inteiras com =, mas não podem ser comparadas diretamente com == — é preciso uma função de comparação própria.

Exercícios

Nível fácil

  1. Defina, com typedef, uma struct Livro com os membros titulo (char[100]), autor (char[100]) e ano (int). Na main(), declare uma variável desse tipo, preencha seus membros e imprima-os.
  2. Defina uma struct Retangulo com membros largura e altura (ambos float). Escreva uma função float area(Retangulo r) que calcule e retorne a área do retângulo.
  3. Declare um vetor de 3 structs Ponto (com membros x e y, inteiros), inicialize cada uma com valores diferentes, e imprima todas em um laço for.

Nível médio

  1. Defina uma struct Aluno (nome, idade, nota). Escreva uma função void aumentarNota(Aluno *a, float pontos) que receba um ponteiro para um Aluno e some pontos à sua nota, alterando o aluno original. Use o operador -> na implementação.
  2. Escreva uma função Aluno melhorAluno(Aluno turma[], int tamanho) que percorra um vetor de Aluno e retorne (por valor) o aluno com a maior nota.
  3. 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

  1. Defina uma struct Data (dia, mes, ano) e uma struct Evento que contenha um char nome[50] e uma Data data (struct aninhada). Escreva uma função int depoisDe(Data d1, Data d2) que retorne 1 se d1 for uma data posterior a d2, e 0 caso 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 de Evento.
  2. 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);
}