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
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 6.7 (unions) e 6.8 (enums, via
#definee constantes) - cppreference — Enumerations — https://en.cppreference.com/w/c/language/enum
- cppreference — Union declaration — https://en.cppreference.com/w/c/language/union
- Modern C, Jens Gustedt — seções sobre enumerações e uniões — https://gustedt.gitlabpages.inria.fr/modern-c/
- 21st Century C, Ben Klemens (O'Reilly) — discussão prática sobre tagged unions
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.