Unions e campos de bits

1. Introdução às Unions

Em Linguagem C, uma union é um tipo de dado que permite armazenar diferentes tipos de dados no mesmo espaço de memória. Diferentemente de uma struct, onde cada membro ocupa seu próprio endereço, todos os membros de uma union compartilham o mesmo local de memória.

A sintaxe básica é semelhante à de uma struct:

union exemplo {
    int inteiro;
    float flutuante;
    char caractere;
};

Para declarar e acessar membros:

#include <stdio.h>

union Dado {
    int i;
    float f;
    char c;
};

int main() {
    union Dado d;
    d.i = 42;
    printf("Inteiro: %d\n", d.i);
    d.f = 3.14;
    printf("Float: %.2f\n", d.f);
    // Após atribuir d.f, d.i foi sobrescrito
    printf("Inteiro após float: %d\n", d.i); // Valor imprevisível
    return 0;
}

2. Funcionamento da Memória em Unions

O princípio fundamental da union é que todos os membros ocupam o mesmo endereço inicial. O tamanho total da union é determinado pelo maior membro declarado.

#include <stdio.h>

union Teste {
    char c;      // 1 byte
    int i;       // 4 bytes
    double d;    // 8 bytes
};

int main() {
    union Teste t;
    printf("Endereço de t.c: %p\n", &t.c);
    printf("Endereço de t.i: %p\n", &t.i);
    printf("Endereço de t.d: %p\n", &t.d);
    printf("Tamanho da union: %zu bytes\n", sizeof(t));
    // Todos os endereços são iguais, tamanho = 8 bytes
    return 0;
}

Isso significa que escrever em um membro automaticamente corrompe os dados dos outros membros. Para evitar acessos inválidos, é responsabilidade do programador controlar qual membro está ativo.

3. Aplicações Práticas de Unions

Economia de memória em sistemas embarcados:

union Sensor {
    int temperatura;
    unsigned int pressao;
    float umidade;
};

Tagged unions (união com discriminador):

#include <stdio.h>

enum Tipo { INT, FLOAT, STRING };

struct Valor {
    enum Tipo tipo;
    union {
        int i;
        float f;
        char* s;
    } dado;
};

void imprimir(struct Valor v) {
    switch(v.tipo) {
        case INT:    printf("%d\n", v.dado.i); break;
        case FLOAT:  printf("%.2f\n", v.dado.f); break;
        case STRING: printf("%s\n", v.dado.s); break;
    }
}

int main() {
    struct Valor v1 = {INT, .dado.i = 10};
    struct Valor v2 = {FLOAT, .dado.f = 3.14};
    imprimir(v1);
    imprimir(v2);
    return 0;
}

Manipulação de bytes de um inteiro:

#include <stdio.h>

union BytesInt {
    int valor;
    unsigned char bytes[sizeof(int)];
};

int main() {
    union BytesInt bi;
    bi.valor = 0x12345678;
    for(int i = 0; i < sizeof(int); i++) {
        printf("Byte %d: 0x%02X\n", i, bi.bytes[i]);
    }
    return 0;
}

4. Introdução aos Campos de Bits

Campos de bits permitem especificar o número exato de bits que um membro de uma struct deve ocupar. A sintaxe utiliza dois pontos seguidos da largura em bits:

struct Flags {
    unsigned int ativo : 1;
    unsigned int modo : 2;
    unsigned int prioridade : 3;
    unsigned int : 2;  // bits de preenchimento
};

Exemplo prático de empacotamento:

#include <stdio.h>

struct Pacote {
    unsigned int versao : 4;
    unsigned int tipo : 4;
    unsigned int tamanho : 8;
    unsigned int checksum : 8;
};

int main() {
    struct Pacote p = {2, 5, 100, 0xFF};
    printf("Versão: %u\n", p.versao);
    printf("Tipo: %u\n", p.tipo);
    printf("Tamanho: %u\n", p.tamanho);
    printf("Checksum: 0x%02X\n", p.checksum);
    printf("Tamanho total: %zu bytes\n", sizeof(p));
    return 0;
}

5. Funcionamento e Armazenamento de Campos de Bits

O compilador aloca campos de bits em unidades de armazenamento (geralmente 4 bytes). A ordem de bits depende da arquitetura (endianness) e do compilador.

#include <stdio.h>

struct Bits {
    unsigned int a : 3;
    unsigned int b : 5;
    unsigned int c : 8;
};

int main() {
    struct Bits b = {7, 31, 255};
    printf("Tamanho: %zu bytes\n", sizeof(b));
    // A ordem dos bits na memória é dependente de implementação
    return 0;
}

Limitações importantes:
- Tipos suportados: int, unsigned int, signed int (e _Bool em C99)
- Não é possível obter endereço de um campo de bits (&b.a é inválido)
- Não podem ser usados em sizeof
- Arrays de campos de bits não são permitidos

6. Aplicações Práticas de Campos de Bits

Representação de registradores de hardware:

struct Registrador {
    unsigned int enable : 1;
    unsigned int mode : 2;
    unsigned int interrupt : 1;
    unsigned int reserved : 4;
    unsigned int status : 8;
};

Estruturas compactas para protocolos:

struct CabecalhoTCP {
    unsigned int source_port : 16;
    unsigned int dest_port : 16;
    unsigned int seq_num : 32;
    unsigned int ack_num : 32;
    unsigned int data_offset : 4;
    unsigned int reserved : 3;
    unsigned int flags : 9;
    unsigned int window : 16;
    unsigned int checksum : 16;
    unsigned int urgent_ptr : 16;
};

Otimização de flags booleanos:

struct Configuracao {
    unsigned int debug : 1;
    unsigned int verbose : 1;
    unsigned int log : 1;
    unsigned int cache : 1;
    unsigned int ssl : 1;
    unsigned int compress : 1;
    unsigned int : 2;  // preenchimento
};

int main() {
    struct Configuracao cfg = {1, 0, 1, 0, 1, 0};
    printf("Tamanho: %zu byte(s)\n", sizeof(cfg));
    return 0;
}

7. Boas Práticas e Cuidados

Portabilidade: O comportamento de campos de bits pode variar entre compiladores e arquiteturas. A ordem de bits, o alinhamento e o tipo de int usado são dependentes de implementação.

Alternativas modernas: Em muitos casos, máscaras de bits e operadores bitwise oferecem mais controle e portabilidade:

#define FLAG_DEBUG   (1 << 0)
#define FLAG_VERBOSE (1 << 1)
#define FLAG_LOG     (1 << 2)

unsigned int flags = 0;
flags |= FLAG_DEBUG;
if (flags & FLAG_DEBUG) { /* ... */ }

Quando evitar:
- Quando a portabilidade entre diferentes compiladores é crítica
- Em código que precisa ser depurado facilmente (campos de bits são difíceis de inspecionar)
- Quando o desempenho é mais importante que a economia de memória (acessos a campos de bits podem gerar código mais lento)
- Em interfaces de rede ou arquivos binários que precisam seguir um layout exato

Recomendações finais:
- Sempre use unsigned int para campos de bits a menos que sinais sejam necessários
- Documente claramente o layout esperado dos bits
- Considere usar union com campos de bits e tipos inteiros para inspeção direta
- Teste em diferentes plataformas se a portabilidade for necessária

Referências