Uma struct diz "todos estes, juntos". Uma union diz "exatamente um destes, por vez". Os membros são colocados uns sobre os outros no mesmo endereço, então a union é apenas tão grande quanto o seu maior membro, e escrever um membro destrói os outros.
Isso soa como uma armadilha, e sem controle é mesmo. Mas também é como o C expressa um valor que pode ser de vários tipos - um token que é um número ou uma string, uma mensagem que é um clique ou uma tecla - sem pagar por todos de uma vez.
Declarando e usando uma union
A sintaxe espelha exatamente a de uma struct; só a palavra-chave muda.
Os membros são acessados com . (ou -> através de um ponteiro), exatamente como numa struct. O que muda é que apenas o membro escrito mais recentemente guarda um valor com sentido. Depois de v.f = 3.5f, ler v.i não dá 3 - dá o inteiro qualquer que o padrão de bits de 3.5f por acaso soletra.
Tamanho: o maior membro vence
Compare o leiaute de memória de uma struct e de uma union com os mesmos membros:
Numa máquina típica a struct tem 24 bytes (4 para o int, 8 para o double, 1 para o char, mais preenchimento) enquanto a union tem 8 - o tamanho do seu double, arredondado por alinhamento. Os três endereços impressos são idênticos, que é toda a história das unions numa linha de saída.
Repare nas conversões para void * no %p. O printf espera exatamente isso para %p; passar outro tipo de ponteiro é comportamento indefinido, ainda que normalmente pareça funcionar. Veja especificadores de formato.
Inicializando uma union
Um inicializador entre chaves sem designador inicializa o primeiro membro:
A forma designada é a que se deve usar. {42} depende silenciosamente da ordem dos membros, então reordenar a declaração depois muda qual membro é inicializado - um bug genuinamente cruel, porque nada no código parece diferente.
O problema de verdade: qual membro está vivo?
Uma union não registra qual membro você escreveu por último. São só bytes; o conhecimento mora na sua cabeça, e é exatamente daí que o conhecimento some.
O número impresso é grande e estranho - o padrão de bits de 1.0f lido como int. Nada travou, nada avisou, e o programa está silenciosamente errado. A union fez exatamente o que prometeu; o erro foi nosso, ao esquecer qual membro estava vivo.
A solução: uma união etiquetada
A solução padrão é emparelhar a union com um enum que registra o membro vivo, e envolver os dois numa struct. Essa combinação se chama união etiquetada (ou união discriminada), e é como você deveria escrever essencialmente toda union em código de aplicação.
Toda leitura agora passa pelo switch sobre kind, então é impossível ler um membro que nunca foi escrito - contanto que toda escrita também defina a etiqueta. Envolver as escritas em pequenas funções construtoras (value_from_int, value_from_string) é a forma usual de tornar isso impossível de esquecer.
A economia de memória é real: cada Value aqui custa 24 bytes de carga mais a etiqueta, em vez de 4 + 4 + 24 de uma struct guardando os três. Com cem mil deles, isso importa.
Compilar com -Wall acrescenta uma segunda rede de proteção: se você acrescentar depois um VAL_BOOL ao enum e esquecer um case para ele, o GCC avisa sobre o valor de enumeração não tratado.
Unions anônimas
O C11 permite um membro union sem nome dentro de uma struct, promovendo os membros dele ao espaço de nomes da struct externa:
Como a própria union não tem nome, você escreve s->circle.r em vez de s->as.circle.r. Mais curto de ler, ao custo de esconder o fato de que há uma union envolvida - o que tudo bem quando a etiqueta está bem ali do lado.
Onde as unions realmente se justificam
Quatro usos recorrentes:
- Valores variantes. Interpretadores, analisadores de JSON e de configuração, e filas de mensagens todos carregam valores cujo tipo é decidido em tempo de execução. Uma união etiquetada é a representação canônica.
- Registros apertados em memória. Quando uma struct tem vários campos mutuamente exclusivos e você tem milhões deles, sobrepô-los é uma economia direta.
- Leiautes de protocolo e de hardware. Um pacote cuja carga depende de um byte de cabeçalho mapeia naturalmente numa união etiquetada, assim como registradores de dispositivo.
- Inspeção de bytes. Sobrepor um valor a um array
unsigned char[]permite olhar os bytes individuais dele, por exemplo para determinar a ordem dos bytes (endianness):
Isso é type punning - ler bytes deliberadamente de volta como outro tipo. Ler através de uma union assim é explicitamente permitido como resultado definido pela implementação em C (ao contrário de converter ponteiros entre tipos sem relação, o que quebra as regras de aliasing), e inspecionar bytes como unsigned char é sempre seguro. Reinterpretar um int como float é outra história: o resultado depende inteiramente da representação da sua plataforma, então mantenha isso fora de código portável.
Erros comuns
- Ler um membro que você não escreveu. O perigo principal. Use uma etiqueta.
- Supor que uma union converte. Ela não converte.
u.i = 3; float f = u.f;reinterpreta bits;int i = 3; float f = i;converte. Veja conversão de tipos. - Colocar um ponteiro numa union e perdê-lo de vista. Se um ramo guarda um
char *que você alocou, sobrescrever a union com outro membro o vaza - nada mais aponta para o buffer. Libere antes de trocar de ramo. - Esperar que o compilador verifique. Ele não vai. Unions são um dos poucos recursos do C em que a linguagem não oferece ajuda nenhuma além de tamanho e alinhamento; a etiqueta é a sua única grade de proteção.
Perguntas frequentes
O que é uma union em C?
Uma union é um tipo cujos membros compartilham a mesma memória. Escrever um membro sobrescreve os outros, então uma union guarda exatamente um dos seus membros por vez. É declarada como uma struct, mas com a palavra-chave union: union Value { int i; float f; };.
Qual a diferença entre uma union e uma struct em C?
Uma struct dá a cada membro o seu próprio armazenamento e guarda todos ao mesmo tempo, então o tamanho dela é pelo menos a soma dos membros. Uma union sobrepõe todos os membros no mesmo endereço e guarda um por vez, então o tamanho dela é o do maior membro.
Qual o tamanho de uma union em C?
Grande o bastante para o maior membro, arredondado para cima por alinhamento. Uma union de um int (4 bytes) e um double (8 bytes) tem 8 bytes - não 12. O sizeof é a forma de conferir na sua própria plataforma.
O que acontece se você ler um membro da union que não escreveu?
Você reinterpreta os mesmos bytes como um tipo diferente. Escrever u.i = 1 e depois ler u.f não converte - lê o padrão de bits do inteiro como float, dando um número sem sentido. No C padrão isso é, na melhor das hipóteses, não especificado, e é por isso que existe o padrão de união etiquetada: manter ao lado uma etiqueta dizendo qual membro está vivo.