Unions e Enums: Economia e Clareza

[361] Unions e Enums: Economia e Clareza

Uma struct reserva espaço para todos os campos ao mesmo tempo; a union, para um só — e ocupa o tamanho do maior deles. Dessa diferença nasce o padrão mais elegante do C, a union etiquetada, em que um enum diz qual campo vale agora. Antes dela vem o enum sozinho, que troca o obscuro estado == 2 por um nome legível.
Linguagem C

10 min de leitura

Você já domina as structs, o principal tipo composto do C. Hoje conhecemos seus dois primos menos famosos, mas igualmente úteis: a union, que economiza memória deixando vários tipos compartilharem o mesmo espaço, e o enum, que troca números mágicos por nomes legíveis. Cada um resolve um problema distinto — um de espaço, outro de clareza —, e juntos completam o conjunto de ferramentas para você desenhar tipos que expressem exatamente o seu problema.

Enum: dando nomes a números

Comece pelo mais simples e mais imediatamente útil. Muitas vezes, uma variável só faz sentido assumindo um pequeno conjunto de valores: um dia da semana, o estado de um semáforo, o naipe de uma carta. Representá-los com números crus (0, 1, 2) funciona, mas o código fica obscuro — o que significa estado == 2? O enum resolve isso dando nomes a esses valores:

#include <stdio.h>

enum Semaforo { VERMELHO, AMARELO, VERDE };

int main(void) {
    enum Semaforo estado = VERMELHO;

    if (estado == VERMELHO) {
        printf("Pare!\n");
    }

    estado = VERDE;
    printf("Valor numérico de VERDE: %d\n", estado); // 2
    return 0;
}

Um enum define um conjunto de constantes inteiras com nomes. Por padrão, elas começam em 0 e crescem de um em um: aqui, VERMELHO é 0, AMARELO é 1, VERDE é 2. A grande vantagem é a legibilidade: if (estado == VERMELHO) diz exatamente o que significa, enquanto if (estado == 0) obriga o leitor a decifrar. O código passa a falar a linguagem do problema, não a dos números.

Controlando os valores do enum

Você pode atribuir valores explícitos às constantes, quando eles importam:

#include <stdio.h>

enum CodigoHttp {
    OK = 200,
    NAO_ENCONTRADO = 404,
    ERRO_SERVIDOR = 500
};

// Se você atribui um valor e deixa o próximo em branco,
// ele continua a partir do anterior:
enum Mes {
    JANEIRO = 1, // começa em 1
    FEVEREIRO,   // 2, automaticamente
    MARCO,       // 3
    ABRIL        // 4
};

int main(void) {
    enum CodigoHttp resposta = NAO_ENCONTRADO;
    printf("Código: %d\n", resposta); // 404
    printf("Abril é o mês %d\n", ABRIL); // 4
    return 0;
}

Isso é útil quando os números têm significado externo (códigos HTTP, por exemplo) ou quando você quer que a contagem comece em 1, como nos meses. Onde você não especifica, o valor continua incrementando a partir do último definido. Enums brilham especialmente em switch: como vimos na aula de controle de fluxo, um switch sobre um enum, cobrindo cada caso nomeado, produz um código autoexplicativo e fácil de manter.

Enum combinado com struct e switch

Um uso muito comum é marcar o "tipo" ou o "estado" de algo dentro de uma struct:

#include <stdio.h>

enum FormaTipo { CIRCULO, QUADRADO };

struct Forma {
    enum FormaTipo tipo;
    double dimensao; // raio, se círculo; lado, se quadrado
};

double area(struct Forma f) {
    switch (f.tipo) {
        case CIRCULO:
            return 3.14159 * f.dimensao * f.dimensao;
        case QUADRADO:
            return f.dimensao * f.dimensao;
        default:
            return 0.0;
    }
}

int main(void) {
    struct Forma c = {CIRCULO, 5.0};
    struct Forma q = {QUADRADO, 4.0};
    printf("Área do círculo: %.2f\n", area(c)); // 78.54
    printf("Área do quadrado: %.2f\n", area(q)); // 16.00
    return 0;
}

O campo tipo, do tipo enum, deixa claro o que a struct representa, e o switch trata cada caso pelo nome. Esse padrão — uma struct com um campo enum indicando sua variante — é a base de estruturas mais sofisticadas, e conduz naturalmente ao próximo conceito.

Union: um espaço, vários tipos

A union parece uma struct, mas guarda um segredo importante: enquanto uma struct reserva espaço para todos os seus campos ao mesmo tempo, uma union reserva espaço para apenas um campo de cada vez — todos os campos compartilham a mesma região de memória. O tamanho da union é o do seu maior campo:

#include <stdio.h>

union Valor {
    int inteiro;
    float decimal;
    char letra;
};

int main(void) {
    union Valor v;

    v.inteiro = 42;
    printf("Como inteiro: %d\n", v.inteiro); // 42

    v.decimal = 3.14f; // sobrescreve o MESMO espaço
    printf("Como decimal: %.2f\n", v.decimal); // 3.14
    // v.inteiro agora contém lixo — o espaço foi reescrito como float!

    printf("Tamanho da union: %zu bytes\n", sizeof(v)); // 4 (o maior campo)
    return 0;
}

A regra de ouro da union: só um campo é válido por vez. Quando você escreve em v.decimal, o valor que estava em v.inteiro é sobrescrito — eles ocupam os mesmos bytes. Ler um campo diferente do que você escreveu por último produz uma reinterpretação dos bits, geralmente sem sentido. A union economiza memória (ela ocupa o tamanho do maior campo, não a soma de todos), mas transfere a você a responsabilidade de lembrar qual campo está ativo.

O padrão clássico: a union etiquetada

Como a union sozinha não sabe qual campo está ativo, o idioma consagrado em C é combiná-la com um enum que funciona como "etiqueta" (tag), guardados juntos numa struct. Assim você tem, com segurança, um valor que pode ser de vários tipos:

#include <stdio.h>

enum TipoDado { INTEIRO, DECIMAL, TEXTO };

struct Variante {
    enum TipoDado tipo;     // a etiqueta: diz qual campo está ativo
    union {
        int inteiro;
        double decimal;
        char texto[20];
    } valor;                // a union: guarda o dado
};

void imprimir(struct Variante v) {
    switch (v.tipo) {
        case INTEIRO: printf("int: %d\n", v.valor.inteiro); break;
        case DECIMAL: printf("double: %.2f\n", v.valor.decimal); break;
        case TEXTO:   printf("texto: %s\n", v.valor.texto); break;
    }
}

int main(void) {
    struct Variante a = {INTEIRO, .valor.inteiro = 42};
    struct Variante b = {DECIMAL, .valor.decimal = 3.14};

    imprimir(a); // int: 42
    imprimir(b); // double: 3.14
    return 0;
}

Este é o padrão tagged union (union etiquetada), um dos mais elegantes do C. A etiqueta tipo diz qual campo da union é o válido; a função de leitura consulta a etiqueta antes de acessar o campo certo. É assim que o C representa "um valor que pode ser de vários tipos" com segurança — o mesmo conceito que linguagens modernas oferecem como enums com dados ou uniões marcadas. Reconhecer esse padrão vai te ajudar a entender código de interpretadores, parsers e bibliotecas de baixo nível.

Quando usar cada um

Vale resumir os papéis. Use um enum sempre que uma variável assumir um conjunto pequeno e fixo de valores nomeados — é ganho puro de legibilidade, sem contrapartida, e você deveria alcançá-lo com frequência. Use uma union quando precisar economizar memória guardando um entre vários tipos possíveis, tipicamente dentro do padrão tagged union — é uma ferramenta mais especializada, que você usará com menos frequência, mas que é insubstituível quando o problema pede. E lembre-se sempre da diferença essencial: a struct guarda todos os campos simultaneamente; a union guarda um de cada vez, no mesmo espaço.

Fechando o essencial dos tipos

Com structs, enums e unions, você agora domina todas as ferramentas do C para criar seus próprios tipos. Na próxima aula, vamos aprender a torná-los mais agradáveis de usar com o typedef, que elimina a repetição do struct a cada declaração e nos permite dar nomes limpos aos nossos tipos — um passo pequeno na sintaxe, mas grande na legibilidade, e que prepara o terreno para organizarmos projetos maiores nas aulas seguintes.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Defina um enum DiaSemana com os sete dias (começando DOMINGO = 0). Declare uma variável, atribua QUARTA e imprima seu valor numérico. Confirme que é 3.

Ver resposta

✓ Resposta:

#include <stdio.h>
enum DiaSemana { DOMINGO, SEGUNDA, TERCA, QUARTA, QUINTA, SEXTA, SABADO };
int main(void) {
    enum DiaSemana d = QUARTA;
    printf("%d\n", d); // 3
    return 0;
}

Como DOMINGO é 0, a contagem automática torna QUARTA igual a 3.

Exercício 2

Defina um enum Naipe { OUROS, ESPADAS, COPAS, PAUS } e escreva uma função const char *nome_naipe(enum Naipe n) que retorne o nome do naipe como string, usando um switch.

Ver resposta

✓ Resposta:

#include <stdio.h>
enum Naipe { OUROS, ESPADAS, COPAS, PAUS };

const char *nome_naipe(enum Naipe n) {
    switch (n) {
        case OUROS:   return "Ouros";
        case ESPADAS: return "Espadas";
        case COPAS:   return "Copas";
        case PAUS:    return "Paus";
        default:      return "Desconhecido";
    }
}

int main(void) {
    printf("%s\n", nome_naipe(COPAS)); // Copas
    return 0;
}

A função retorna um ponteiro para uma string literal — segura de retornar porque literais existem durante toda a execução do programa.

Exercício 3

Declare uma union Numero com um campo int e um campo double. Escreva um valor no campo int, imprima-o, depois escreva no campo double e imprima. Explique por que ler o campo int depois de escrever no double não devolveria mais o valor original.

Ver resposta

✓ Resposta:

#include <stdio.h>
union Numero { int i; double d; };
int main(void) {
    union Numero n;
    n.i = 100;
    printf("%d\n", n.i); // 100
    n.d = 2.5;
    printf("%.1f\n", n.d); // 2.5
    return 0;
}

Ler n.i depois de escrever n.d não devolveria 100 porque os dois campos compartilham a mesma memória. Ao escrever n.d = 2.5, os bytes que guardavam o inteiro 100 foram sobrescritos pela representação em ponto flutuante de 2.5. Ler n.i agora interpretaria esses bytes como inteiro, produzindo um número sem relação com o original — só um campo é válido por vez.

Exercício 4

Use sizeof para comparar o tamanho de uma struct com três campos (int, double, char) e o de uma union com os mesmos três campos. Explique a diferença nos tamanhos observados.

Ver resposta

✓ Resposta:

#include <stdio.h>
struct S { int i; double d; char c; };
union U  { int i; double d; char c; };
int main(void) {
    printf("struct: %zu\n", sizeof(struct S)); // tipicamente 16 (com padding)
    printf("union:  %zu\n", sizeof(union U));   // tipicamente 8
    return 0;
}

A struct reserva espaço para todos os campos ao mesmo tempo, então seu tamanho é a soma dos campos mais algum padding de alinhamento (comumente 16 bytes: 4 do int + 8 do double + 1 do char, arredondado por alinhamento). A union reserva espaço para apenas um campo por vez, então seu tamanho é o do maior campo (o double, 8 bytes). Essa é a diferença essencial: struct soma, union usa o maior.

Exercício 5

Implemente um mini "valor dinâmico" com o padrão tagged union: uma struct com um enum { INT, FLOAT } e uma union com um int e um float. Escreva uma função que receba essa struct e imprima o valor corretamente conforme a etiqueta. Teste com um de cada tipo.

Ver resposta

✓ Resposta:

#include <stdio.h>

enum Tipo { INT, FLOAT };

struct Valor {
    enum Tipo tipo;
    union {
        int i;
        float f;
    } dado;
};

void imprimir(struct Valor v) {
    switch (v.tipo) {
        case INT:   printf("int: %d\n", v.dado.i); break;
        case FLOAT: printf("float: %.2f\n", v.dado.f); break;
    }
}

int main(void) {
    struct Valor a = {INT, .dado.i = 7};
    struct Valor b = {FLOAT, .dado.f = 3.5f};
    imprimir(a); // int: 7
    imprimir(b); // float: 3.50
    return 0;
}

A função consulta a etiqueta v.tipo antes de acessar a union, garantindo que lê o campo correto — o coração seguro do padrão tagged union.

Comentários

Mais em Linguagem C

Automação de Build com Make
Automação de Build com Make

Indentar o comando com espaço em vez de TAB gera um erro enigmático — é o…

O Pré-processador: #define, Macros e Inclusões
O Pré-processador: #define, Macros e Inclusões

O quadrado de 2 + 3 dá 11 quando a macro esquece os parênteses — o…

Sinais Unix e Eventos do Sistema Operacional
Sinais Unix e Eventos do Sistema Operacional

Se o sinal chega no meio de um malloc e o manipulador também aloca, a…