Campos de bits (bit fields) em C são uma forma de definir membros de uma estrutura que ocupam um número específico de bits, em vez de bytes inteiros. Isso é útil quando você precisa armazenar valores que cabem em poucos bits, como flags booleanas ou pequenos inteiros, permitindo economizar memória e tornar o código mais expressivo. Nesta aula, veremos como declarar e usar campos de bits, suas vantagens, limitações e aplicações práticas.

Embora pareçam simples, campos de bits têm particularidades quanto à portabilidade e alinhamento que devem ser compreendidas para evitar surpresas. Vamos explorar cada aspecto com exemplos concretos.

Sintaxe

A sintaxe para declarar um campo de bits é semelhante à de um membro de estrutura comum, mas com a adição de dois pontos e o número de bits que o campo deve ocupar. O tipo base deve ser um tipo inteiro (int, unsigned int, signed int ou _Bool). O número de bits especificado não pode exceder o número de bits do tipo base.

Exemplo básico:

struct Flags {
    unsigned int flag1 : 1;
    unsigned int flag2 : 1;
    unsigned int valor : 4;
};

Neste exemplo, flag1 e flag2 ocupam 1 bit cada (podem armazenar 0 ou 1), e valor ocupa 4 bits (pode armazenar de 0 a 15). O uso de unsigned int é recomendado para evitar ambiguidades com sinal.

Você também pode usar signed int para campos com sinal, mas isso pode reduzir a faixa útil (por exemplo, 1 bit com sinal pode armazenar -1 ou 0). Para evitar confusão, prefira sempre unsigned a menos que sinal seja necessário.

Campos de bits podem ser combinados com membros normais em uma estrutura:

struct Pacote {
    unsigned int tipo : 4;
    unsigned int prioridade : 2;
    unsigned int erro : 1;
    unsigned int : 1;  // campo anônimo de preenchimento
    unsigned int payload : 8;
};

Campos anônimos (sem nome) são usados para preenchimento (padding) e não podem ser acessados. O campo unsigned int : 1; força o próximo campo a começar em um novo byte ou alinhamento, dependendo da implementação.

Economia de memória

A principal motivação para usar campos de bits é economizar memória, especialmente quando você precisa armazenar muitas variáveis pequenas. Por exemplo, se você tem 8 flags booleanas, em vez de usar 8 bytes (ou 8 variáveis int), você pode usar uma estrutura com 8 campos de 1 bit, ocupando apenas 1 byte (ou menos, dependendo do alinhamento).

Considere uma estrutura para representar os atributos de um personagem em um jogo:

struct Atributos {
    unsigned int forca : 6;   // 0-63
    unsigned int destreza : 6;
    unsigned int inteligencia : 6;
    unsigned int carisma : 6;
    unsigned int sorte : 4;   // 0-15
}; // Total: 28 bits → 4 bytes (com alinhamento)

Se usássemos unsigned int para cada atributo (32 bits cada), gastaríamos 20 bytes. Com campos de bits, reduzimos para 4 bytes (considerando alinhamento típico).

É importante notar que a economia real depende do compilador e da arquitetura. O compilador pode adicionar bits de preenchimento para alinhar os campos aos limites de bytes ou palavras. Em muitos sistemas, campos de bits não ultrapassam o limite de um unsigned int (geralmente 32 bits), então estruturas com muitos campos podem exigir múltiplos inteiros.

Para maximizar a economia, é comum agrupar campos de bits em uma única estrutura e evitar misturar com membros de tamanho maior, que podem quebrar o alinhamento.

Portabilidade

Campos de bits têm comportamento definido pela implementação em vários aspectos, o que pode afetar a portabilidade do código. As principais questões são:

  • Ordem dos bits: Não é garantido se os bits são alocados do mais significativo para o menos significativo ou vice-versa. Isso depende da arquitetura (endianness) e do compilador.
  • Alinhamento: O compilador decide como posicionar os campos dentro da unidade de armazenamento (geralmente um unsigned int). Pode haver bits de preenchimento entre campos ou no final.
  • Tipo base: O padrão permite que campos de bits sejam declarados com int, signed int ou unsigned int. O uso de int pode ser interpretado como signed ou unsigned dependendo da implementação.
  • Limite de tamanho: O número máximo de bits em um campo é limitado pelo tamanho do tipo base. Em muitos sistemas, unsigned int tem 32 bits, mas pode ser 16 em sistemas embarcados.

Para escrever código portável, evite assumir a ordem dos bits. Se você precisar de um layout binário específico (por exemplo, para comunicação com hardware), é melhor usar máscaras e deslocamentos manuais com inteiros comuns. Campos de bits são mais adequados para uso interno, onde a portabilidade não é crítica.

Exemplo de problema de portabilidade:

struct Exemplo {
    unsigned int a : 4;
    unsigned int b : 4;
};

Em uma máquina little-endian, a pode ocupar os 4 bits menos significativos, enquanto em big-endian, pode ocupar os mais significativos. Se você tentar enviar essa estrutura pela rede, a interpretação será diferente.

Casos de uso

Campos de bits são amplamente utilizados em:

  • Flags e opções: Em sistemas operacionais, bibliotecas gráficas e jogos, para armazenar múltiplos estados booleanos de forma compacta.
  • Protocolos de comunicação: Para representar cabeçalhos de pacotes com campos de tamanho fixo em bits (ex.: TCP, IP).
  • Registradores de hardware: Para mapear registradores de dispositivos onde cada bit tem um significado específico.
  • Compressão de dados: Para armazenar valores com precisão reduzida (ex.: 10 bits para um sensor).
  • Estruturas de dados compactas: Em sistemas embarcados com memória limitada.

Exemplo prático: Representação de um cabeçalho Ethernet simplificado (campos de bits para versão e comprimento):

struct EthernetHeader {
    unsigned int versao : 4;
    unsigned int comprimento : 4;
    unsigned int tipo : 8;
    // ... outros campos
};

Outro exemplo: Um sistema de configuração com múltiplas opções:

struct Config {
    unsigned int opcaoA : 1;
    unsigned int opcaoB : 1;
    unsigned int opcaoC : 1;
    unsigned int : 5; // preenchimento para completar 8 bits
};

Com isso, você pode passar a estrutura como um único byte para economizar espaço em arquivos de configuração.

Boas práticas

  • Use unsigned para campos de bits a menos que sinal seja explicitamente necessário.
  • Evite assumir a ordem ou alinhamento dos bits; não serialize estruturas com campos de bits diretamente (use máscaras e shifts).
  • Prefira campos de bits para flags internas; para protocolos binários, use inteiros comuns e manipulação de bits.
  • Documente o layout esperado se o código for usado em múltiplas plataformas.

Referências

Exercícios

  1. Crie uma estrutura chamada Status com campos de bits para representar as seguintes flags: ativo (1 bit), visivel (1 bit), bloqueado (1 bit) e prioridade (3 bits). Escreva um trecho de código que inicializa uma variável dessa estrutura com ativo = 1, visivel = 0, bloqueado = 0 e prioridade = 5.

    ✓ Resposta:
    struct Status {
        unsigned int ativo : 1;
        unsigned int visivel : 1;
        unsigned int bloqueado : 1;
        unsigned int prioridade : 3;
    };
    
    struct Status s = {1, 0, 0, 5};
  2. Qual é o tamanho em bytes da estrutura Flags definida a seguir? Justifique.

    struct Flags {
        unsigned int a : 1;
        unsigned int b : 2;
        unsigned int c : 3;
    };

    ✓ Resposta: O tamanho total em bits é 1+2+3 = 6 bits. O compilador geralmente aloca um unsigned int (32 bits) como unidade de armazenamento, então o tamanho será de 4 bytes (32 bits). Embora apenas 6 bits sejam usados, os bits restantes são preenchidos. Em alguns compiladores, se a estrutura couber em um byte, pode ser 1 byte, mas o padrão permite que seja 4. A resposta mais segura é 4 bytes.
  3. Explique por que campos de bits não são portáveis para representar protocolos de rede. Dê um exemplo de problema que pode ocorrer.

    ✓ Resposta: Campos de bits não são portáveis porque a ordem de alocação dos bits (se o primeiro campo ocupa os bits mais ou menos significativos) é definida pela implementação. Em uma máquina little-endian, os primeiros campos podem ocupar os bits menos significativos, enquanto em big-endian, ocupam os mais significativos. Ao enviar a estrutura pela rede, a interpretação seria diferente. Além disso, o compilador pode inserir bits de preenchimento entre campos, alterando o layout. Portanto, para protocolos de rede, é melhor usar inteiros comuns e manipular bits com máscaras e deslocamentos.
  4. Escreva uma função que recebe um valor inteiro de 0 a 255 e o armazena em uma estrutura com um campo de bits de 8 bits. A função deve retornar o valor armazenado. Use unsigned int como tipo base.

    ✓ Resposta:
    struct Byte {
        unsigned int valor : 8;
    };
    
    unsigned int armazenar(unsigned int v) {
        struct Byte b = {v};
        return b.valor;
    }
  5. Considere a estrutura abaixo. Quantos bits de preenchimento (padding) existem entre os campos x e y? E depois de y?

    struct Exemplo {
        unsigned int x : 3;
        unsigned int : 0; // campo anônimo de largura zero
        unsigned int y : 5;
    };

    ✓ Resposta: O campo anônimo de largura zero força o alinhamento ao próximo unsigned int (geralmente 32 bits). Portanto, após x (3 bits), o campo de largura zero pula para o próximo inteiro, adicionando 29 bits de preenchimento antes de y. Depois de y (5 bits), o restante do inteiro (27 bits) é preenchido, totalizando 32 bits para o segundo inteiro. A estrutura ocupa 8 bytes.