Structs: Criando Seus Próprios Tipos

[356] Structs: Criando Seus Próprios Tipos

Três variáveis soltas dão conta de uma data; de cem, não. A struct cola o que pertence junto — e traz a diferença que a separa do vetor: ela é um valor de verdade, copiada por inteiro na atribuição e na chamada. Também estão aqui o aninhamento de campos, a inicialização designada do C99 e o strcpy que o nome exige.
Linguagem C

10 min de leitura

Até agora, você guardou números, caracteres e sequências deles. Mas o mundo real não vem em pedaços soltos: um aluno tem nome, idade e nota, tudo junto; um ponto no plano tem coordenada x e y, inseparáveis. Manter esses dados em variáveis avulsas é frágil e desorganizado. Hoje você aprende a criar seus próprios tipos, agrupando informações relacionadas num único bloco coeso — as structs. É o primeiro passo para modelar problemas de verdade em C, e a fundação sobre a qual construiremos, mais adiante, as estruturas de dados.

O problema que as structs resolvem

Imagine representar uma data. Sem structs, você espalha três variáveis:

int dia = 15;
int mes = 6;
int ano = 2026;

Funciona para uma data. Mas e para cem datas? E para passar uma data a uma função — três argumentos toda vez? E se você acidentalmente misturar o mes de uma data com o ano de outra? Os dados que pertencem juntos deveriam viver juntos. É isso que a struct faz: ela cola campos relacionados num único tipo.

Declarando uma struct

Uma struct é definida com a palavra-chave struct, um nome, e os campos entre chaves:

#include <stdio.h>

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

int main(void) {
    struct Data hoje;      // declara uma variável do tipo struct Data
    hoje.dia = 15;
    hoje.mes = 6;
    hoje.ano = 2026;

    printf("%02d/%02d/%d\n", hoje.dia, hoje.mes, hoje.ano);
    return 0;
}

Repare em dois pontos. O tipo criado se chama struct Data — as duas palavras juntas (voltaremos a isso com typedef, mais adiante). E o operador ponto (.) acessa cada campo: hoje.dia, hoje.mes. Cada variável struct Data tem seu próprio conjunto de campos, independente das outras. O %02d no printf, a propósito, formata o número com dois dígitos, preenchendo com zero à esquerda — daí sair 15/06/2026 em vez de 15/6/2026.

Inicializando de uma vez

Como nos vetores, você pode preencher os campos na declaração:

#include <stdio.h>

struct Ponto {
    double x;
    double y;
};

int main(void) {
    struct Ponto origem = {0.0, 0.0};   // por ordem dos campos
    struct Ponto p = {.x = 3.0, .y = 4.0}; // por nome (mais claro)

    printf("origem: (%.1f, %.1f)\n", origem.x, origem.y);
    printf("p:      (%.1f, %.1f)\n", p.x, p.y);
    return 0;
}

A segunda forma, {.x = 3.0, .y = 4.0}, é a inicialização designada (do C99). Ela nomeia cada campo explicitamente, o que torna o código mais legível e imune a erros de ordem — se um dia você reorganizar os campos da struct, a inicialização por nome continua correta. Prefira-a quando a clareza importar.

Structs contêm qualquer tipo — inclusive outras structs

Um campo pode ser de qualquer tipo: inteiro, double, vetor, ou até outra struct. Isso permite compor modelos ricos:

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

struct Data {
    int dia, mes, ano;   // vários campos do mesmo tipo, na mesma linha
};

struct Pessoa {
    char nome[50];        // um vetor de char como campo
    int idade;
    struct Data nascimento; // uma struct dentro de outra
};

int main(void) {
    struct Pessoa ana;
    strcpy(ana.nome, "Ana Silva");  // copiamos a string (não se atribui com =)
    ana.idade = 30;
    ana.nascimento.dia = 12;         // acesso encadeado com pontos
    ana.nascimento.mes = 3;
    ana.nascimento.ano = 1996;

    printf("%s, %d anos, nascida em %02d/%02d/%d\n",
           ana.nome, ana.idade,
           ana.nascimento.dia, ana.nascimento.mes, ana.nascimento.ano);
    return 0;
}

Note ana.nascimento.dia: para chegar a um campo de uma struct aninhada, você encadeia os pontos. E observe que o campo nome é um vetor de char — para preenchê-lo, usamos strcpy, não =, pela mesma razão que vimos na aula de strings: não se copia um vetor com atribuição simples.

A diferença fundamental: struct é copiada por valor

Aqui está algo que distingue a struct do vetor, e que você precisa gravar. Ao contrário de um vetor (que decai para ponteiro), uma struct é um valor de verdade: quando você a atribui ou a passa para uma função, ela é copiada por inteiro, campo a campo:

#include <stdio.h>

struct Ponto { int x, y; };

int main(void) {
    struct Ponto a = {1, 2};
    struct Ponto b = a;   // CÓPIA completa: b é independente de a

    b.x = 99;             // altera só b
    printf("a.x = %d\n", a.x); // 1 — a não mudou
    printf("b.x = %d\n", b.x); // 99
    return 0;
}

b = a copia os dois campos. Depois disso, a e b são objetos separados; mexer em um não afeta o outro. Essa é uma diferença crucial em relação aos vetores. Ela é conveniente (você trabalha com o valor sem medo de efeitos colaterais), mas tem um custo: copiar uma struct grande a cada chamada de função pode ser caro. É por isso que, muitas vezes, passamos structs para funções por ponteiro — exatamente o tema da próxima aula.

Passando structs para funções

Você pode passar uma struct para uma função como qualquer valor. A função recebe uma cópia:

#include <stdio.h>

struct Retangulo {
    double largura;
    double altura;
};

double area(struct Retangulo r) {   // recebe uma CÓPIA de r
    return r.largura * r.altura;
}

struct Retangulo escalar(struct Retangulo r, double fator) {
    r.largura *= fator;   // altera só a cópia local
    r.altura  *= fator;
    return r;             // devolve uma nova struct
}

int main(void) {
    struct Retangulo caixa = {4.0, 3.0};
    printf("Área: %.1f\n", area(caixa)); // 12.0

    struct Retangulo dobro = escalar(caixa, 2.0);
    printf("Dobro: %.1f x %.1f\n", dobro.largura, dobro.altura); // 8.0 x 6.0
    printf("Original intacto: %.1f x %.1f\n", caixa.largura, caixa.altura); // 4.0 x 3.0
    return 0;
}

Como escalar recebe uma cópia, ela pode modificá-la à vontade sem tocar no original — e devolve a versão modificada como um novo valor. Esse estilo, que trata structs como valores imutáveis e retorna versões novas, é limpo e seguro. Quando você quiser que a função altere o original, ou quando a struct for grande demais para copiar, aí entra o ponteiro.

Um exemplo que modela um problema

Vamos juntar tudo para representar uma conta bancária simples:

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

struct Conta {
    char titular[50];
    int numero;
    double saldo;
};

struct Conta depositar(struct Conta c, double valor) {
    c.saldo += valor;
    return c;
}

void extrato(struct Conta c) {
    printf("Conta %d (%s): R$ %.2f\n", c.numero, c.titular, c.saldo);
}

int main(void) {
    struct Conta minha = {.numero = 1001, .saldo = 500.0};
    strcpy(minha.titular, "Marcelo Fontes");

    extrato(minha);                    // R$ 500.00
    minha = depositar(minha, 250.0);   // reatribuímos o resultado
    extrato(minha);                    // R$ 750.00
    return 0;
}

Repare como a struct organiza o problema: em vez de três variáveis soltas para cada conta, temos um tipo Conta que anda junto. As funções recebem e devolvem Conta inteiras, e o código lê quase como a descrição do domínio.

O que vem a seguir

Hoje você deu um salto de expressividade: deixou de usar apenas os tipos prontos do C e passou a criar os seus, agrupando dados relacionados em structs. Vimos como declará-las, inicializá-las, aninhá-las, e — o ponto central — que structs são copiadas por valor, ao contrário dos vetores. Essa cópia é conveniente, mas às vezes indesejável ou custosa. Na próxima aula, vamos combinar structs com o que aprendemos na Fase 2 e acessá-las por ponteiro — o que traz um novo operador, a seta (->), e abre caminho para as estruturas de dados dinâmicas.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Defina uma struct Livro com campos titulo (string de até 100 caracteres), ano (int) e preco (double). Crie uma variável, preencha-a e imprima os três campos formatados.

Ver resposta

✓ Resposta:

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

struct Livro {
    char titulo[100];
    int ano;
    double preco;
};

int main(void) {
    struct Livro l;
    strcpy(l.titulo, "The C Programming Language");
    l.ano = 1988;
    l.preco = 89.90;
    printf("%s (%d) — R$ %.2f\n", l.titulo, l.ano, l.preco);
    return 0;
}

Exercício 2

Defina uma struct Ponto com campos x e y (double). Escreva uma função double distancia(struct Ponto a, struct Ponto b) que calcule a distância euclidiana entre dois pontos (use sqrt de <math.h>; compile com -lm).

Ver resposta

✓ Resposta: A distância euclidiana é a raiz da soma dos quadrados das diferenças:

#include <stdio.h>
#include <math.h>

struct Ponto { double x, y; };

double distancia(struct Ponto a, struct Ponto b) {
    double dx = a.x - b.x;
    double dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
}

int main(void) {
    struct Ponto p = {0.0, 0.0};
    struct Ponto q = {3.0, 4.0};
    printf("%.2f\n", distancia(p, q)); // 5.00
    return 0;
}

Compile com gcc arquivo.c -o prog -lm — o -lm liga a biblioteca matemática, necessária para sqrt.

Exercício 3

Demonstre, com um pequeno programa, que atribuir uma struct a outra (b = a) cria uma cópia independente: altere um campo de b e mostre que o campo correspondente de a permaneceu inalterado.

Ver resposta

✓ Resposta:

#include <stdio.h>
struct Ponto { int x, y; };
int main(void) {
    struct Ponto a = {10, 20};
    struct Ponto b = a;   // cópia
    b.x = 99;
    printf("a.x = %d, b.x = %d\n", a.x, b.x); // 10, 99
    return 0;
}

O a.x continua 10: b é uma cópia completa e independente de a, então alterar b.x não afeta a.

Exercício 4

Defina uma struct Horario (campos hora, minuto, segundo) e uma struct Evento que contenha um nome (string) e dois campos Horario: inicio e fim. Preencha um evento e imprima seus horários de início e fim.

Ver resposta

✓ Resposta:

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

struct Horario { int hora, minuto, segundo; };

struct Evento {
    char nome[80];
    struct Horario inicio;
    struct Horario fim;
};

int main(void) {
    struct Evento e;
    strcpy(e.nome, "Aula de C");
    e.inicio = (struct Horario){19, 0, 0};
    e.fim    = (struct Horario){20, 30, 0};

    printf("%s: %02d:%02d — %02d:%02d\n",
           e.nome,
           e.inicio.hora, e.inicio.minuto,
           e.fim.hora, e.fim.minuto);
    return 0;
}

O (struct Horario){...} é um literal composto (C99), uma forma prática de criar uma struct temporária para atribuir de uma vez.

Exercício 5

Escreva uma função struct Ponto ponto_medio(struct Ponto a, struct Ponto b) que retorne o ponto médio entre dois pontos (média das coordenadas). Teste com (0,0) e (4,6), esperando (2,3).

Ver resposta

✓ Resposta:

#include <stdio.h>
struct Ponto { double x, y; };

struct Ponto ponto_medio(struct Ponto a, struct Ponto b) {
    struct Ponto m;
    m.x = (a.x + b.x) / 2.0;
    m.y = (a.y + b.y) / 2.0;
    return m;
}

int main(void) {
    struct Ponto a = {0.0, 0.0};
    struct Ponto b = {4.0, 6.0};
    struct Ponto m = ponto_medio(a, b);
    printf("(%.1f, %.1f)\n", m.x, m.y); // (2.0, 3.0)
    return 0;
}

A função monta uma nova struct com as médias das coordenadas e a devolve por valor — o chamador recebe uma cópia do resultado.

Comentários

Mais em Linguagem C

Quando o Programa Decide: if, else e switch
Quando o Programa Decide: if, else e switch

Trocar == por = dentro de um if compila, roda e faz a coisa errada em…

Por que C existe e o que ele quer de você
Por que C existe e o que ele quer de você

Em 1972, reescrever o Unix a cada máquina nova era trabalho hercúleo — foi…

Undefined Behavior: O Território Perigoso do C
Undefined Behavior: O Território Perigoso do C

Um índice a mais num vetor de cinco pode imprimir tudo certo, travar, ou virar…