C Unions

Uma struct guarda todos os seus campos ao mesmo tempo: nome e idade e nota. A union é parecida no jeito de escrever, mas guarda um campo de cada vez: todos eles moram no mesmo pedaço da memória, e gravar um apaga o outro. Parece estranho, mas resolve um problema comum: uma informação que pode ser de tipos diferentes, conforme o caso. Na lojinha da Ana, um pagamento em dinheiro tem o valor recebido (para calcular o troco), um pagamento no cartão tem o número de parcelas, e um Pix tem a chave. Nunca os três juntos. Nesta aula você vai ver a union por dentro, aprender a usá-la com segurança (junto com um enum) e espiar os bytes de um número.

Você vai aprender
  • A diferença entre struct e union, vista no sizeof e nos endereços.
  • Por que só o último membro gravado vale, e o que sai quando você lê outro.
  • Como criar uma union já com valor: {7}, {.real = 2.5}, {0}.
  • A union marcada: uma struct com um enum que diz qual membro da union está valendo.
  • A union anônima do C11, que dispensa um nome no meio do caminho.
  • Como usar uma union para ver os bytes de um float e de um inteiro.

Todos os programas desta aula foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa. A aula usa structs, enums e o operador -> (aula Structs e Ponteiros).


A ideia: uma vaga de garagem

O prédio da Ana tem um depósito com várias prateleiras, uma para cada coisa: bicicleta, caixa de ferramentas, cadeira de praia. Tudo cabe ao mesmo tempo, e o depósito precisa ter o tamanho de todas as prateleiras somadas. Isso é a struct.

Já a vaga de garagem recebe um carro ou uma moto ou uma bicicleta. Uma coisa por vez. Ela precisa ter só o tamanho da maior delas, o carro. Se a Ana estaciona a moto, o carro não está mais lá. E se alguém chega procurando o carro numa vaga onde está a moto, vai encontrar… uma moto. Isso é a union.

struct (o depósito)union (a vaga)
Quantos membros guardam valor ao mesmo tempoTodosUm só: o último gravado
TamanhoA soma de todos (mais o enchimento)O do maior membro (mais o enchimento)
Endereço de cada membroUm diferente para cadaO mesmo para todos
Para que serveJuntar informações que andam juntasUma informação que pode ter tipos diferentes

O nome vem do inglês union, "união": vários tipos unidos no mesmo lugar.


1. struct × union: o tamanho

A melhor forma de entender a union é colocá-la ao lado de uma struct com os mesmos membros. A escrita é idêntica; só muda a palavra do começo:

#include <stdio.h>

struct Caixa {
    int inteiro;
    double real;
    char letra;
};

union Gaveta {
    int inteiro;
    double real;
    char letra;
};

int main(void) {
    printf("sizeof(int)          = %zu\n", sizeof(int));
    printf("sizeof(double)       = %zu\n", sizeof(double));
    printf("sizeof(char)         = %zu\n", sizeof(char));
    printf("sizeof(struct Caixa) = %zu\n", sizeof(struct Caixa));
    printf("sizeof(union Gaveta) = %zu\n", sizeof(union Gaveta));
    return 0;
}
union Gaveta { … };
Declara um tipo novo, como a struct: union + etiqueta + membros entre chaves + ;. Cada membro é tipo + nome.
int inteiro; double real; char letra;
Os membros (o nome usado na union, já que "campo" lembra algo que existe ao lado dos outros). A union pode guardar um int ou um double ou um char.
sizeof(union Gaveta)
O tamanho da union inteira, em bytes (aula sizeof).
$ gcc -std=c23 -Wall -Wextra tamanho.c -o tamanho
$ ./tamanho
sizeof(int)          = 4
sizeof(double)       = 8
sizeof(char)         = 1
sizeof(struct Caixa) = 24
sizeof(union Gaveta) = 8

A union tem 8 bytes: o tamanho do maior membro, o double. Nela cabe um double inteiro, ou um int (usando 4 dos 8 bytes), ou um char (usando 1). A struct tem 24: os 4 + 8 + 1 = 13 bytes dos campos e mais 11 de enchimento (padding), que o compilador põe para cada double começar num endereço múltiplo de 8 (veja Structs). O editor do guia mostra os mesmos números.


2. Todos os membros no mesmo endereço

Para ver que os membros da union moram no mesmo lugar, imprima o endereço de cada um com %p (aula Memória e Endereços):

#include <stdio.h>

union Valor {
    int inteiro;
    double real;
    char letra;
};

struct Ficha {
    int inteiro;
    double real;
    char letra;
};

int main(void) {
    union Valor v;
    struct Ficha f;
    printf("union:  %p %p %p\n", (void *)&v.inteiro, (void *)&v.real, (void *)&v.letra);
    printf("struct: %p %p %p\n", (void *)&f.inteiro, (void *)&f.real, (void *)&f.letra);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra endereco.c -o endereco
$ ./endereco
union:  0x7fff99a00558 0x7fff99a00558 0x7fff99a00558      « o mesmo endereço, três vezes
struct: 0x7fff99a00560 0x7fff99a00568 0x7fff99a00570      « um endereço para cada campo

Na union, os três membros começam no mesmo byte. Na struct, cada campo tem o seu lugar, um depois do outro. Os números mudam a cada execução e no editor do guia saem bem menores (0x7ffff0), porque lá a memória é organizada de outro jeito; o que importa é a comparação entre eles.


3. Só o último gravado vale

Se todos os membros ocupam o mesmo lugar, gravar um escreve por cima dos outros:

#include <stdio.h>

union Valor {
    int inteiro;
    double real;
};

int main(void) {
    union Valor v;

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

    v.real = 3.75;
    printf("real    = %.2f\n", v.real);
    printf("inteiro = %d\n", v.inteiro);
    return 0;
}
union Valor v;
Cria uma variável do tipo union Valor (as duas palavras, como em struct Aluno). Por enquanto, 8 bytes com lixo.
v.inteiro = 42;
O acesso é com o ponto, igual à struct. Grava 42 nos 4 primeiros bytes.
v.real = 3.75;
Grava o double 3.75 nos 8 bytes, por cima do 42.
printf(…, v.inteiro)
Lê os 4 primeiros bytes como se fossem um int. Mas eles agora são um pedaço do 3.75.
$ gcc -std=c23 -Wall -Wextra primeira.c -o primeira
$ ./primeira
inteiro = 42
real    = 3.75
inteiro = 0              « o 42 sumiu: ali estão bytes do 3.75

O 42 não existe mais. O 0 que apareceu não é "zerado": são os 4 primeiros bytes do double 3.75, que por acaso são todos zero. Com outro número, sairia outra coisa. Repare que nem o gcc nem o Clang avisam: a union não guarda qual membro foi gravado por último. Lembrar disso é trabalho seu, e a seção 5 mostra como fazer o programa lembrar.

Experimente: rode e confira o inteiro = 0. Depois troque o 3.75 por 3.1 e veja o "inteiro" virar um número negativo enorme, sem sentido. Por último, mude a ordem: grave o real primeiro e o inteiro depois. Quem sobrevive agora?

#include <stdio.h>

union Valor {
    int inteiro;
    double real;
};

int main(void) {
    union Valor v;

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

    v.real = 3.75;
    printf("real    = %.2f\n", v.real);
    printf("inteiro = %d\n", v.inteiro);
    printf("sizeof(v) = %zu\n", sizeof v);
    return 0;
}

4. Criar a union já com valor

Como na struct, você pode dar valor na criação, com chaves. Mas só um valor, já que só um membro vale por vez:

#include <stdio.h>

union Valor {
    int inteiro;
    double real;
};

int main(void) {
    union Valor a = {7};
    union Valor b = {.real = 2.5};
    union Valor c = {0};
    union Valor d = {2.5};
    printf("a.inteiro = %d\n", a.inteiro);
    printf("b.real    = %.1f\n", b.real);
    printf("c.inteiro = %d, c.real = %.1f\n", c.inteiro, c.real);
    printf("d.inteiro = %d\n", d.inteiro);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra iniciar.c -o iniciar
$ ./iniciar
a.inteiro = 7
b.real    = 2.5
c.inteiro = 0, c.real = 0.0
d.inteiro = 2              « o 2.5 foi para o inteiro e perdeu o .5
EscritaO que faz
{7}Sem nome, o valor vai para o primeiro membro da lista (inteiro).
{.real = 2.5}Com o designador C99, você escolhe o membro. É a forma mais clara.
{0} e {}Tudo zerado ({} a partir do C23).
{2.5}A armadilha: sem designador, o 2.5 vai para o primeiro membro, que é int, e vira 2. O gcc não avisa nada. O Clang do editor avisa: implicit conversion from 'double' to 'int' changes value from 2.5 to 2 [-Wliteral-conversion].

Regra prática: numa union, use sempre o designador (.membro = valor). Assim ninguém precisa lembrar qual membro é o primeiro.


5. A union marcada: o enum diz qual membro vale

Sozinha, a union é perigosa: o programa não sabe qual membro está valendo. Por isso, quase sempre ela anda dentro de uma struct, ao lado de um enum que serve de etiqueta: "aqui dentro está um cartão". Esse par se chama union marcada (tagged union). É o uso mais importante da union. Veja o caixa da lojinha:

#include <stdio.h>

enum Forma { DINHEIRO, CARTAO, PIX };

struct Pagamento {
    enum Forma forma;
    double valor;
    union {
        double recebido;      /* DINHEIRO: quanto o cliente deu */
        int parcelas;         /* CARTAO: em quantas vezes */
        char chave[40];       /* PIX: a chave usada */
    } info;
};

void mostrar(const struct Pagamento *p) {
    printf("R$ %6.2f ", p->valor);
    switch (p->forma) {
    case DINHEIRO:
        printf("em dinheiro, troco de R$ %.2f\n", p->info.recebido - p->valor);
        break;
    case CARTAO:
        printf("no cartão, em %dx de R$ %.2f\n", p->info.parcelas, p->valor / p->info.parcelas);
        break;
    case PIX:
        printf("por Pix, chave %s\n", p->info.chave);
        break;
    }
}

int main(void) {
    struct Pagamento vendas[3] = {
        {.forma = DINHEIRO, .valor = 37.50, .info.recebido = 50.00},
        {.forma = CARTAO, .valor = 120.00, .info.parcelas = 3},
        {.forma = PIX, .valor = 15.90, .info.chave = "ana@lojinha.com"},
    };
    for (int i = 0; i < 3; i++) {
        mostrar(&vendas[i]);
    }
    printf("sizeof(struct Pagamento) = %zu\n", sizeof(struct Pagamento));
    return 0;
}
enum Forma { DINHEIRO, CARTAO, PIX };
As três formas de pagamento (aula Enums). Cada uma diz qual membro da union está valendo.
enum Forma forma;
A etiqueta (a "marca"). Ela fica fora da union, para nunca ser apagada.
double valor;
O que todo pagamento tem, qualquer que seja a forma, também fica fora da union.
union { … } info;
Uma union sem etiqueta, declarada ali mesmo, como membro info da struct. Só um dos três vale em cada pagamento.
.info.recebido = 50.00
Designador em dois níveis: o membro info da struct, e dentro dele o recebido.
switch (p->forma)
Antes de ler a union, olha a etiqueta. Cada case lê só o membro que combina com ela. Como forma é um enum, o -Wall avisa se faltar uma forma no switch.
p->info.parcelas
A seta entra na struct pelo ponteiro, e o ponto entra na union.
$ gcc -std=c23 -Wall -Wextra pagamento.c -o pagamento
$ ./pagamento
R$  37.50 em dinheiro, troco de R$ 12.50
R$ 120.00 no cartão, em 3x de R$ 40.00
R$  15.90 por Pix, chave ana@lojinha.com
sizeof(struct Pagamento) = 56

56 bytes: 4 do enum, 4 de enchimento, 8 do valor e 40 da union (o tamanho da chave, o maior membro). Com os três campos separados numa struct (recebido, parcelas e chave lado a lado), o mesmo pagamento ocuparia 72 bytes. A economia é pequena aqui, mas o ganho maior é outro: o tipo struct Pagamento diz, com clareza, que cada pagamento tem uma dessas informações, e nunca as três.

A regra de ouro da union marcada

Sempre que gravar um membro da union, atualize a etiqueta na mesma hora; sempre que for ler, olhe a etiqueta primeiro. Uma forma de não esquecer é criar funções que fazem as duas coisas juntas, como struct Pagamento pagar_pix(double valor, const char *chave).


6. C11 Union anônima: sem o nome do meio

Escrever p.info.parcelas toda hora é comprido. A partir do C11, a union dentro da struct pode não ter nome nenhum. Os membros dela passam a ser usados como se fossem da struct:

#include <stdio.h>

enum Forma { DINHEIRO, CARTAO, PIX };

struct Pagamento {
    enum Forma forma;
    double valor;
    union {
        double recebido;
        int parcelas;
        char chave[40];
    };
};

int main(void) {
    struct Pagamento p = {.forma = CARTAO, .valor = 120.00, .parcelas = 4};
    printf("%dx de R$ %.2f\n", p.parcelas, p.valor / p.parcelas);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra anonima.c -o anonima
$ ./anonima
4x de R$ 30.00

Repare: union { … }; sem nome antes do ;, e o acesso é p.parcelas, direto. Por dentro, nada mudou: recebido, parcelas e chave continuam dividindo o mesmo lugar. A missão final usa essa forma.


7. Espiar os bytes de um número

A union tem um segundo uso, mais técnico: gravar por um membro e ler por outro de propósito, para ver como o computador guarda um valor. Em C, isso é permitido: os bytes são relidos como o outro tipo. (Em C++, não é; lá se usa memcpy.) Este programa mostra os 32 bits de um float:

#include <stdio.h>
#include <stdint.h>

union Numero {
    float f;
    uint32_t bits;
};

int main(void) {
    union Numero n;
    n.f = 1.0f;
    printf("%.1f fica guardado como 0x%08X\n", n.f, n.bits);
    n.f = -1.0f;
    printf("%.1f fica guardado como 0x%08X\n", n.f, n.bits);
    n.f = 0.1f;
    printf("%.1f fica guardado como 0x%08X\n", n.f, n.bits);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra bits.c -o bits
$ ./bits
1.0 fica guardado como 0x3F800000
-1.0 fica guardado como 0xBF800000
0.1 fica guardado como 0x3DCCCCCD

O uint32_t é um inteiro sem sinal de exatamente 32 bits, do tamanho do float (aula stdint.h). O %08X mostra o número em hexadecimal com 8 dígitos. Repare que 1.0 e -1.0 diferem só no primeiro dígito: o bit mais alto do float é o sinal. E o 0.1 termina em CCCCCD, uma dízima arredondada: é por isso que 0.1 não é exato no computador (aula float e double).

O mesmo truque mostra a ordem dos bytes de um inteiro:

#include <stdio.h>
#include <stdint.h>

union Pedacos {
    uint32_t numero;
    unsigned char byte[4];
};

int main(void) {
    union Pedacos p;
    p.numero = 0x12345678;
    for (int i = 0; i < 4; i++) {
        printf("byte[%d] = %02x\n", i, p.byte[i]);
    }
    return 0;
}
$ gcc -std=c23 -Wall -Wextra bytes.c -o bytes
$ ./bytes
byte[0] = 78
byte[1] = 56
byte[2] = 34
byte[3] = 12

O número 0x12345678 ficou guardado "de trás para frente": o byte 78, o menos importante, vem primeiro na memória. Esse jeito se chama little-endian, e é o dos processadores Intel, AMD e da maioria dos ARM. O editor do guia (WebAssembly) também é little-endian e mostra o mesmo. A aula Arquivos Binários mostra por que isso importa quando o número vai para um arquivo.


struct, union ou variáveis separadas: qual usar

SituaçãoUseExemplo
Várias informações que existem ao mesmo tempostructAluno com nome, idade e nota.
Uma informação que pode ser de tipos diferentes, um por vezunion marcada (struct + enum + union)Pagamento em dinheiro, cartão ou Pix; uma célula de planilha com número ou texto.
Ver os bytes de um valorunion de dois tipos do mesmo tamanhofloat + uint32_t.
Poucos casos e memória sobrandostruct com todos os camposMais simples de entender; gasta um pouco mais.
Union "solta", sem etiquetaEviteNinguém sabe qual membro vale: convite para ler lixo.

Economizar memória era o grande motivo da union nos computadores antigos. Hoje, para um programa comum, alguns bytes não fazem diferença, e a union aparece mais pela clareza (a union marcada) e em código de baixo nível: protocolos de rede, formatos de arquivo, programas para microcontroladores com pouca memória.


Quando dá errado

Números sem sentido: leu o membro errado
    struct Pagamento p = {.forma = PIX, .valor = 15.90, .info.chave = "ana@lojinha.com"};
    printf("Parcelas: %d\n", p.info.parcelas);
    printf("Recebido: %g\n", p.info.recebido);
$ gcc -std=c23 -Wall -Wextra errado.c -o errado
$ ./errado
Parcelas: 1080127073
Recebido: 6.3234e+199

O que aconteceu: o pagamento é um Pix, então o que está na union é o texto "ana@lojinha.com". O programa leu esses bytes como int e como double. O 1080127073 são as letras a, n, a, @ lidas como número (em hexadecimal, 0x40616e61: os códigos de @, a, n e a, a palavra de trás para frente, por causa da ordem dos bytes da seção 7). Nenhum aviso, nenhum erro: só dados errados, e o editor do guia mostra os mesmos números.

Como resolver: antes de ler a union, olhe a etiqueta (if (p.forma == CARTAO) ou um switch), como no pagamento.c.

"unknown type name ‘Valor’; use ‘union’ keyword"
$ gcc -std=c23 -Wall -Wextra semunion.c -o semunion
semunion.c: In function ‘main’:
semunion.c:9:5: error: unknown type name ‘Valor’; use ‘union’ keyword to refer to the type
    9 |     Valor v;
      |     ^~~~~
      |     union
semunion.c:10:6: error: request for member ‘inteiro’ in something not a structure or union
   10 |     v.inteiro = 3;
      |      ^
«…»

O que aconteceu: como na struct e no enum, o nome do tipo são as duas palavras: union Valor. O segundo erro é consequência do primeiro: request for member ‘inteiro’ in something not a structure or union = "pediu o membro inteiro de algo que não é struct nem union". O Clang do editor diz must use 'union' tag to refer to type 'Valor'.

Como resolver: escreva union Valor v;, ou crie um apelido com typedef: typedef union { … } Valor;.

"‘union Valor’ has no member named ‘texto’"
$ gcc -std=c23 -Wall -Wextra membro.c -o membro
membro.c: In function ‘main’:
membro.c:9:23: error: ‘union Valor’ has no member named ‘texto’
    9 |     union Valor v = {.texto = "oi"};
      |                       ^~~~~
membro.c:9:31: error: initialization of ‘int’ from ‘char *’ makes integer from pointer without a cast [-Wint-conversion]
    9 |     union Valor v = {.texto = "oi"};
      |                               ^~~~
membro.c:9:31: note: (near initialization for ‘v.inteiro’)

O que aconteceu: a union só tem inteiro e real. O designador .texto não existe, e o gcc ainda tentou pôr o "oi" no primeiro membro, o que gerou o segundo erro. O Clang do editor diz field designator 'texto' does not refer to any field in type 'union Valor'.

Como resolver: confira o nome do membro na declaração, ou acrescente o membro que falta (char texto[20];).

"initialized field overwritten": dois valores na mesma union
$ gcc -std=c23 -Wall -Wextra duasvezes.c -o duasvezes
duasvezes.c: In function ‘main’:
duasvezes.c:9:44: warning: initialized field overwritten [-Woverride-init]
    9 |     union Valor v = {.inteiro = 1, .real = 2.0};
      |                                            ^~~
duasvezes.c:9:44: note: (near initialization for ‘v.real’)
$ ./duasvezes
2.0

O que aconteceu: "campo inicializado foi sobrescrito". Numa union só cabe um valor; o .real = 2.0 apagou o .inteiro = 1. O Clang do editor diz initializer overrides prior initialization of this subobject.

Como resolver: dê valor a um membro só. Se você precisa dos dois ao mesmo tempo, o tipo certo é uma struct.

A etiqueta diz uma coisa, a union guarda outra

O que aconteceu: o programa mudou p.parcelas = 2; mas esqueceu o p.forma = CARTAO;. Para o resto do programa, o pagamento continua sendo em dinheiro, e o "valor recebido" agora é o lixo da seção 3. O compilador não tem como perceber.

Como resolver: grave o membro e a etiqueta sempre juntos, de preferência numa função só (a dica da seção 5).


Referência rápida

EscritaSignificado
union Valor { int inteiro; double real; };Declara o tipo: um int ou um double no mesmo lugar.
union Valor v;Cria uma variável (com lixo, se for local).
v.real = 2.5;Grava num membro e apaga o que havia nos outros.
sizeof(union Valor)O tamanho do maior membro, mais o enchimento.
union Valor v = {.real = 2.5};Cria com valor num membro escolhido (C99).
union Valor v = {7};Sem designador: o valor vai para o primeiro membro.
struct { enum Forma forma; union { … } info; };Union marcada: a etiqueta diz qual membro vale.
struct { …; union { int a; double b; }; };Union anônima (C11): acesso direto, s.a.
p->info.parcelasMembro da union dentro de uma struct, por ponteiro.
union { float f; uint32_t bits; }Grava por um tipo, lê os bytes pelo outro.

Exercícios

1. O tamanho

Quantos bytes tem union { char c; int i; double d; } no gcc do laboratório? (char = 1, int = 4, double = 8)

bytes

2. Escolher o membro

Complete para criar a union Valor v com o membro real valendo 9.5:

union Valor v = {};

3. A palavra que falta

Complete a declaração para que os três membros dividam o mesmo lugar na memória:

Medida { int gramas; double litros; char tamanho; };

4. Depois de v.inteiro = 10; v.real = 1.5;, o que vale v.inteiro?

5. Para que serve o enum numa union marcada?

6. Com union Valor { int inteiro; double real; };, o que acontece em union Valor d = {2.5}; no gcc?

Missão final: a cesta da feira

Missão: na feira, a banana e o queijo são vendidos por quilo (o item guarda kg, um double) e o ovo, por unidade (guarda unidades, um int). Os dois dividem uma union anônima, e o campo tipo diz qual vale. A função total está usando kg para todo mundo, e o ovo saiu de graça. Corrija: olhe o tipo e use o membro certo. A saída deve ser:

Banana: 1.500 kg = R$ 7.35
Ovo: 12 un = R$ 9.60
Queijo: 0.250 kg = R$ 13.00
Total: R$ 29.95
#include <stdio.h>

enum Venda { POR_KG, POR_UNIDADE };

struct Item {
    char nome[20];
    enum Venda tipo;
    double preco;          /* preço do kg ou de uma unidade */
    union {
        double kg;         /* vale quando tipo == POR_KG */
        int unidades;      /* vale quando tipo == POR_UNIDADE */
    };
};

double total(const struct Item *it) {
    return it->preco * it->kg;
}

void mostrar(const struct Item *it) {
    if (it->tipo == POR_KG) {
        printf("%s: %.3f kg", it->nome, it->kg);
    } else {
        printf("%s: %d un", it->nome, it->unidades);
    }
    printf(" = R$ %.2f\n", total(it));
}

int main(void) {
    struct Item cesta[] = {
        {"Banana", POR_KG, 4.90, .kg = 1.5},
        {"Ovo", POR_UNIDADE, 0.80, .unidades = 12},
        {"Queijo", POR_KG, 52.00, .kg = 0.250},
    };
    double soma = 0.0;
    for (int i = 0; i < 3; i++) {
        mostrar(&cesta[i]);
        soma += total(&cesta[i]);
    }
    printf("Total: R$ %.2f\n", soma);
    return 0;
}
#include <stdio.h>

enum Venda { POR_KG, POR_UNIDADE };

struct Item {
    char nome[20];
    enum Venda tipo;
    double preco;          /* preço do kg ou de uma unidade */
    union {
        double kg;         /* vale quando tipo == POR_KG */
        int unidades;      /* vale quando tipo == POR_UNIDADE */
    };
};

double total(const struct Item *it) {
    if (it->tipo == POR_KG) {
        return it->preco * it->kg;
    }
    return it->preco * it->unidades;
}

void mostrar(const struct Item *it) {
    if (it->tipo == POR_KG) {
        printf("%s: %.3f kg", it->nome, it->kg);
    } else {
        printf("%s: %d un", it->nome, it->unidades);
    }
    printf(" = R$ %.2f\n", total(it));
}

int main(void) {
    struct Item cesta[] = {
        {"Banana", POR_KG, 4.90, .kg = 1.5},
        {"Ovo", POR_UNIDADE, 0.80, .unidades = 12},
        {"Queijo", POR_KG, 52.00, .kg = 0.250},
    };
    double soma = 0.0;
    for (int i = 0; i < 3; i++) {
        mostrar(&cesta[i]);
        soma += total(&cesta[i]);
    }
    printf("Total: R$ %.2f\n", soma);
    return 0;
}

Rode primeiro sem mudar nada: o ovo aparece com R$ 0.00. É a seção 3 de novo: os bytes do int 12, lidos como double, dão um número tão pequeno que arredonda para zero.


Resumo

  • Uma union se escreve como uma struct, mas todos os membros ocupam o mesmo lugar: o tamanho é o do maior membro.
  • Só o último membro gravado vale. Ler outro dá os mesmos bytes interpretados como outro tipo, sem aviso nenhum.
  • Crie com designador: union Valor v = {.real = 2.5};. Sem ele, o valor vai para o primeiro membro.
  • O jeito seguro é a union marcada: uma struct com um enum (a etiqueta) e a union. Grave os dois juntos; leia a etiqueta antes da union.
  • A union anônima C11 deixa escrever p.parcelas em vez de p.info.parcelas.
  • Em C, gravar por um membro e ler por outro de propósito mostra os bytes de um valor: 1.0f é 0x3F800000, e os inteiros ficam em little-endian.