Uniões
Uniões são tipos de dados compostos por um conjunto de outros tipos existentes, similar a estruturas, mas que compartilham a mesma memória para todos os seus membros.
O tamanho efetivo de um union
é igual ao tamanho do seu maior membro.
A sintaxe para definição de uma união é exatamente a mesma utilizada para definição de estruturas, porém ao invés de escrevermos struct
, utlizaremos a palavra chave union
.
A regra para estruturas anônimas também se aplicam para definição de union
.
Inicialização de uniões
Ao inicializar uma união utilizando chaves {}
, apenas o primeiro membro de uma união é inicializado a menos que um inicializador designado (adicionado no C99
) seja utilizado.
#include <inttypes.h>
union Num32 {
int32_t i32;
uint32_t u32;
float f32;
};
int main()
{
//Inicializará o campo "i32"
union Num32 a = {5};
//Inicializará o campo "f32"
union Num32 b = {.f32 = 1.0f};
//Não recomendado, pois tentará inicializar "i32" com um float
union Num32 c = {5.0f};
}
Reinterpretação de tipos
Um uso muito comum de uniões é a possibilidade de realizar o que chamamos de reinterpretação de tipos, o que é comumente chamado no inglês de "type punning".
O uso de uniões para esse propósito é permitido desde o C99
, mas é comportamento indefinido no C++ e em versões anteriores do C.
A reinterpretação de tipos seria efetivamente ler o valor de um tipo como se ele fosse outro, ao escrever em um dos membros do union
e ler o valor por outro.
No exemplo abaixo, o uso de um union
para reinterpretar um float
e ler sua representação interna :
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
union ieee754_float {
float f32;
struct {
unsigned int fracao : 23;
unsigned int expoente : 8;
unsigned int sinal : 1;
};
};
float lerFloat()
{
char linha[1024];
fgets(linha, sizeof(linha), stdin);
return strtof(linha, NULL);
}
int main()
{
union ieee754_float valor;
printf("Escreva um ponto flutuante: ");
valor.f32 = lerFloat();
printf("sinal = %u\n"
"expoente = %u\n"
"fracao = %u\n",
valor.sinal, valor.expoente, valor.fracao);
}
Lembrando que o código acima funciona para processadores de arquiteturas ARM e x86 que tenham ordenação de bytes em little endian, porém, o mesmo sistema pode não funcionar em outras arquiteturas devido a forma como variáveis de ponto flutuante e bitfields
são ordenados.
Em sistemas linux é comum a presença do cabeçário iee754.h
que tenta tratar dessas diferenças com diretivas de preprocessador.
Usos de uniões
Existem algumas técnicas interessantes que podem ser feitas com union
, essa seção busca compartilhar essas ideias para que você possa aplicar em seu código.
1 - Bitmask e booleanas
Geralmente ao utilizar o padrão de bitmask e separar várias flags em um único inteiro, temos um padrão que é um pouco mais complexo de utilizar do que bit fields de campos booleanos, mas que é mais flexível para definir ou checar múltiplas variaveis de uma vez.
Utilizando uniões, podemos juntar ambos em um único tipo, o exemplo abaixo demonstra isso extendendo o exemplo de RPG utilizado em enumerações:
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
enum {
ESTADO_PETRIFICADO = 1 << 0,
ESTADO_CONGELADO = 1 << 1,
ESTADO_CONFUSO = 1 << 2,
ESTADO_QUEIMANDO = 1 << 3,
ESTADO_ENVENENADO = 1 << 4,
ESTADO_SANGRANDO = 1 << 5,
ESTADO_DOENTE = 1 << 6,
ESTADO_CAOS = ESTADO_DOENTE | ESTADO_CONFUSO | ESTADO_QUEIMANDO,
};
struct DadosPersonagem {
const char *nome;
int vida;
//União e estrutura anônima
union {
struct {
bool petrificado : 1;
bool congelado : 1;
bool confuso : 1;
bool queimando : 1;
bool envenenado : 1;
bool sangrando : 1;
bool doente : 1;
};
uint32_t estado;
};
};
int main()
{
struct DadosPersonagem personagem = {
.nome = "Jonas",
.vida = 100,
.confuso = true,
.doente = true,
.queimando = true
};
//É possível checar multiplos valores de uma vez
if((personagem.estado & ESTADO_CAOS) == ESTADO_CAOS)
puts("Personagem no estado CAOS");
//Mas também podemos checar individualmente cada bit
if(personagem.doente)
puts("Personagem está doente");
}
2 - Pseudo Polimorfismo
É possível implementar um pseudo polimorfismo utilizando uniões, geralmente utilizando um número inteiro ou enumeração para indicar qual tipo é atualmente representado.
Isso é extremamente útil para representar tipos variantes, campos utilizados para repassar mensagens ou dados similares.
Um exemplo com definições de tipos que poderiam ser usados para representar um JSON :
enum TipoJson{
TIPO_JSON_NUMERO,
TIPO_JSON_STRING,
TIPO_JSON_OBJETO,
TIPO_JSON_BOOLEANA,
TIPO_JSON_LISTA,
TIPO_JSON_NULL,
};
struct CampoJson {
enum TipoJson tipo;
//Todos os tipos da união compartilham a mesma memória
union {
double numero;
const char *string;
bool booleana;
struct CampoJson *lista;
struct ObjetoJson *objeto;
};
};
struct ChaveValorJson {
const char *chave;
struct CampoJson valor;
};
struct ObjetoJson {
size_t tamanho;
struct ChaveValorJson *campos;
};
Outros exemplos de uso real de union
para este propósito :
- A estrutura
INPUT
do windows utilizada na funçãoSendInput
para representar entrada de mouse, teclado ou hardware. - A união
SDL_Event
utilizada para representar qualquer evento de janela ou do sistema pela biblioteca SDL. - A união
XEvent
, utilizada pelo X11, gerenciador de janelas normalmente utilizado no linux, para representar eventos de janela. - A união
iwreq_data
utilizada em conjunto com a funçãoioctl
(que receberá um número indicando o comando, sendo o inteiro que diferencia o tipo neste caso) para receber/enviar dados para o driver de rede no linux.