Unions
Esta aula explica o conceito de unions em C, estruturas que compartilham memória entre seus membros, permitindo economizar espaço e interpretar um mesmo bloco de memória de diferentes formas. São abordados desde a definição e sintaxe até casos de uso práticos e cuidados essenciais para evitar erros comuns.
Unions (uniões) são um recurso da linguagem C que permite armazenar diferentes tipos de dados em um mesmo espaço de memória. Diferentemente das structs, onde cada membro ocupa seu próprio endereço, em uma union todos os membros compartilham o mesmo endereço inicial. O tamanho da union é igual ao tamanho do seu maior membro. Isso possibilita economizar memória e também reinterpretar um mesmo conjunto de bytes de maneiras distintas.
Nesta aula, vamos explorar o funcionamento interno das unions, seus usos práticos (como implementar variantes, economizar espaço em protocolos de comunicação e manipular bits) e os cuidados necessários, especialmente em relação ao aliasing e à leitura de membros inativos.
O que são
Uma union é definida de forma similar a uma struct, mas com a palavra-chave union. Seus membros compartilham o mesmo espaço de memória, ou seja, a union ocupa apenas o suficiente para armazenar o maior de seus membros. Em qualquer instante, apenas um dos membros pode conter um valor significativo; escrever em um membro sobrescreve o valor dos outros.
Exemplo de declaração e uso básico:
#include <stdio.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
data.i = 10;
printf("data.i = %d\n", data.i);
data.f = 220.5;
printf("data.f = %.1f\n", data.f);
// Após atribuir a float, o valor de i foi sobrescrito
printf("data.i agora = %d (lixo)\n", data.i);
return 0;
}A saída mostra que, após atribuir a float, o valor inteiro anterior é perdido. A union tem tamanho igual ao do maior membro (20 bytes, devido ao array de char).
Compartilhamento de memória
O compartilhamento de memória é a característica central das unions. Todos os membros começam no mesmo endereço de memória. Isso significa que modificar um membro altera os bytes que são interpretados por todos os outros membros. O layout na memória é definido pela implementação, mas geralmente não há padding entre os membros (ao contrário das structs).
Exemplo para visualizar o endereço:
#include <stdio.h>
union Test {
char c;
int i;
};
int main() {
union Test t;
printf("Endereço de t.c: %p\n", (void*)&t.c);
printf("Endereço de t.i: %p\n", (void*)&t.i);
printf("Ambos são iguais? %s\n", (&t.c == &t.i) ? "Sim" : "Não");
return 0;
}Isso permite, por exemplo, acessar um inteiro byte a byte através de um array de char:
#include <stdio.h>
union IntBytes {
int value;
unsigned char bytes[sizeof(int)];
};
int main() {
union IntBytes u;
u.value = 0x12345678;
for (int i = 0; i < sizeof(int); i++) {
printf("byte %d: 0x%02x\n", i, u.bytes[i]);
}
return 0;
}Note que a ordem dos bytes depende da endianness da máquina (little-endian ou big-endian).
Casos de uso
Unions são muito úteis em situações onde é necessário economizar memória ou interpretar um mesmo dado de formas diferentes. Alguns casos comuns:
- Variantes (tagged unions): Combinar uma union com um campo enumerado que indica qual membro está ativo. Isso é a base para implementar tipos algébricos em C.
- Protocolos de comunicação: Receber um buffer de bytes e interpretá-lo como diferentes tipos de mensagens, dependendo de um campo de tipo.
- Manipulação de bits: Acessar partes específicas de um inteiro (como campos de bits) de forma portável (embora structs com bit-fields também sirvam).
- Economia de memória: Em sistemas embarcados, onde cada byte conta, unions permitem que uma mesma área de memória seja reutilizada para diferentes propósitos ao longo do tempo.
Exemplo de union com tag (discriminação):
#include <stdio.h>
enum Type { INT, FLOAT, STRING };
struct Data {
enum Type type;
union {
int i;
float f;
char *s;
} value;
};
void print(struct Data d) {
switch(d.type) {
case INT: printf("%d\n", d.value.i); break;
case FLOAT: printf("%f\n", d.value.f); break;
case STRING: printf("%s\n", d.value.s); break;
}
}
int main() {
struct Data d1 = {INT, .value.i=42};
struct Data d2 = {FLOAT, .value.f=3.14};
struct Data d3 = {STRING, .value.s="Olá"};
print(d1);
print(d2);
print(d3);
return 0;
}Cuidados
O uso de unions exige atenção para evitar comportamentos indefinidos e bugs sutis:
- Ler o membro errado: O padrão C99 permite ler um membro diferente do último escrito, desde que seja para acessar a representação de bytes (como no exemplo de bytes de um inteiro). No entanto, para outros tipos, o resultado é dependente de implementação (ou comportamento indefinido em C89). Em C99/C11, a leitura de um membro inativo é permitida, mas o valor obtido é o da representação de objeto do membro ativo (ou seja, você pode reinterpretar os bytes).
- Aliasing: Unions podem ser usadas para fazer aliasing entre tipos, mas o compilador pode otimizar assumindo que ponteiros de tipos diferentes não apontam para o mesmo local. O uso de unions é uma forma segura de realizar aliasing (type-punning) em C, desde que se acesse através do membro da union.
- Padding e alinhamento: Embora unions não tenham padding entre membros, o alinhamento da union é o alinhamento máximo entre seus membros. Isso pode causar desperdício de espaço se membros pequenos forem usados.
- Inicialização: Apenas o primeiro membro de uma union pode ser inicializado na declaração (em C99). Para inicializar um membro diferente, é necessário usar um designador (como
.f = 3.14) ou atribuir após a declaração. - Portabilidade: A ordem dos bytes (endianness) e o tamanho dos tipos podem variar entre plataformas. Código que depende da representação interna (como acessar bytes de um inteiro) pode não ser portável.
Exemplo de comportamento indefinido por leitura inadequada (C89):
union U { int i; float f; };
union U u;
u.i = 42;
printf("%f\n", u.f); // Comportamento indefinido em C89; em C99 é definido pela implementaçãoBoa prática: sempre que possível, utilize uma union com tag para saber qual membro está ativo, e evite ler membros inativos sem necessidade.
Boas práticas
Para usar unions de forma segura e eficiente:
- Preferir unions anônimas dentro de structs para criar variantes com tag.
- Documentar claramente qual membro está ativo em cada ponto do código.
- Evitar unions com tipos de tamanhos muito diferentes, pois o desperdício de memória pode ser grande.
- Para manipulação de bits, considere usar bit-fields ou operadores bit a bit em vez de unions, a menos que a portabilidade não seja crítica.
Referências
- cppreference: Union declaration
- GNU C Manual: Unions
- IBM Documentation: Unions
- Stack Overflow: Purpose of unions in C
- Wikipedia: Union type
- Learn-C.org: Unions
Exercícios
Escreva uma union chamada
Numberque possa armazenar umint, umfloatou umdouble. Atribua um valor inteiro 10 e imprima o valor como float (observe o resultado).✓ Resposta:#include <stdio.h> union Number { int i; float f; double d; }; int main() { union Number num; num.i = 10; printf("Como float: %f\n", num.f); return 0; }Crie uma union que permita acessar um inteiro de 32 bits como dois inteiros de 16 bits (short). Teste com um valor conhecido e verifique a ordem dos bytes.
✓ Resposta:#include <stdio.h> #include <stdint.h> union Int32 { uint32_t full; uint16_t parts[2]; }; int main() { union Int32 u; u.full = 0x12345678; printf("Parte baixa: 0x%04x\n", u.parts[0]); printf("Parte alta: 0x%04x\n", u.parts[1]); // Em little-endian: parts[0]=0x5678, parts[1]=0x1234 return 0; }Implemente uma estrutura que represente um valor que pode ser inteiro ou string (com tag). Escreva uma função que imprima o valor corretamente.
✓ Resposta:#include <stdio.h> #include <string.h> enum Type { INT, STRING }; struct Value { enum Type type; union { int i; char s[100]; } data; }; void printValue(struct Value v) { if (v.type == INT) printf("%d\n", v.data.i); else printf("%s\n", v.data.s); } int main() { struct Value v1 = {INT, .data.i = 42}; struct Value v2 = {STRING}; strcpy(v2.data.s, "Olá mundo"); printValue(v1); printValue(v2); return 0; }Explique por que o código abaixo pode causar problemas e como corrigi-lo:
union U { int a; float b; }; union U u; u.a = 5; printf("%f", u.b);✓ Resposta: O código imprime o valor deu.bquando o membro ativo éu.a. Embora em C99 isso seja permitido (a leitura de um membro inativo é definida pela implementação), o valor obtido é a interpretação dos bytes do inteiro como float, que geralmente resulta em um número sem sentido (lixo). Para evitar confusão, deve-se sempre manter o controle de qual membro está ativo, por exemplo usando uma tag.Qual é o tamanho da union abaixo? Justifique.
union Example { char c; int i[5]; double d; };✓ Resposta: O tamanho da union é igual ao tamanho do maior membro.int i[5]ocupa 5 * sizeof(int) = 20 bytes (assumindo int de 4 bytes).doubleocupa 8 bytes.charocupa 1 byte. Portanto, o maior é o array de ints com 20 bytes. O tamanho da union será 20 bytes (possivelmente com alinhamento, mas como o maior membro já está alinhado para int, o alinhamento da union será o maior entre os membros, que é o alinhamento de double ou int, geralmente 8 bytes em sistemas de 64 bits, então o tamanho final pode ser 24 bytes se o alinhamento exigir padding. Mas o conceito é: o tamanho é o do maior membro, possivelmente com padding para alinhamento.