Campos de bits
Nesta aula, exploramos campos de bits em C, uma funcionalidade que permite definir estruturas com membros que ocupam um número específico de bits, otimizando o uso de memória. Abordamos a sintaxe, vantagens de economia de memória, questões de portabilidade e casos de uso práticos, como flags e protocolos de comunicação.
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 intouunsigned int. O uso deintpode ser interpretado comosignedouunsigneddependendo 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 inttem 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
unsignedpara 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
- cppreference.com: Bit fields
- GeeksforGeeks: Bit Fields in C
- Microsoft C: Bit Fields
- Wikipedia: Bit field
- IBM: Bit fields in C
Exercícios
Crie uma estrutura chamada
Statuscom campos de bits para representar as seguintes flags:ativo(1 bit),visivel(1 bit),bloqueado(1 bit) eprioridade(3 bits). Escreva um trecho de código que inicializa uma variável dessa estrutura comativo = 1,visivel = 0,bloqueado = 0eprioridade = 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};Qual é o tamanho em bytes da estrutura
Flagsdefinida 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 umunsigned 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.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.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 intcomo tipo base.✓ Resposta:struct Byte { unsigned int valor : 8; }; unsigned int armazenar(unsigned int v) { struct Byte b = {v}; return b.valor; }Considere a estrutura abaixo. Quantos bits de preenchimento (padding) existem entre os campos
xey? E depois dey?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óximounsigned int(geralmente 32 bits). Portanto, apósx(3 bits), o campo de largura zero pula para o próximo inteiro, adicionando 29 bits de preenchimento antes dey. Depois dey(5 bits), o restante do inteiro (27 bits) é preenchido, totalizando 32 bits para o segundo inteiro. A estrutura ocupa 8 bytes.