Campos de bits
Nesta aula, você aprenderá sobre campos de bits em C, uma técnica para empacotar múltiplos valores em um único inteiro, economizando memória. Vamos explorar a sintaxe, os benefícios, as questões de portabilidade e os casos de uso mais comuns, com exemplos práticos.
Campos de bits (bit fields) são um recurso da linguagem C que permite declarar membros de uma estrutura com um tamanho específico em bits, em vez de usar o tamanho padrão de um tipo (como unsigned int que normalmente ocupa 32 bits). Isso é útil quando você precisa armazenar valores que ocupam poucos bits, como flags booleanas (0 ou 1) ou pequenos números, e deseja economizar memória ou acessar diretamente os bits de um registrador de hardware.
Nesta aula, vamos detalhar como declarar campos de bits, entender as limitações de portabilidade, e ver exemplos práticos de onde essa técnica é aplicada, como em protocolos de comunicação e sistemas embarcados.
Sintaxe
A sintaxe básica de um campo de bits é semelhante à declaração de um membro de estrutura, mas seguida por 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). Por exemplo:
struct {
unsigned int flag1 : 1; // 1 bit
unsigned int flag2 : 1; // 1 bit
unsigned int valor : 4; // 4 bits (0 a 15)
unsigned int : 2; // 2 bits de preenchimento (sem nome)
} campos;
Nesse exemplo, flag1 e flag2 ocupam 1 bit cada, valor ocupa 4 bits, e há um campo anônimo de 2 bits para preenchimento (padding) que não pode ser acessado. Os campos podem ser usados como variáveis normais da estrutura, mas com restrições: não se pode obter o endereço de um campo de bits (operador &), e eles não podem ser arrays. A atribuição e a leitura são feitas normalmente, mas o compilador gerencia o empacotamento dos bits.
É importante notar que a ordem de alocação dos bits (da esquerda para a direita ou vice-versa) é definida pela implementação, ou seja, depende do compilador e da arquitetura. Isso afeta a portabilidade, como veremos mais adiante.
Economia de memória
O principal benefício dos campos de bits é a economia de memória. Em vez de usar um unsigned int (geralmente 4 bytes) para armazenar um valor booleano, você pode usar um campo de 1 bit. Em estruturas com muitos campos pequenos, isso pode reduzir significativamente o espaço ocupado. Por exemplo, considere uma estrutura que armazena informações de um pixel em uma imagem: coordenadas, cor, transparência, etc. Sem campos de bits, cada campo ocuparia pelo menos 1 byte, mas com campos de bits, podemos compactar vários valores em poucos bytes.
Vamos comparar duas versões:
// Sem campos de bits
struct Pixel {
unsigned int x; // 4 bytes
unsigned int y; // 4 bytes
unsigned char r; // 1 byte
unsigned char g; // 1 byte
unsigned char b; // 1 byte
unsigned char alpha; // 1 byte
}; // total: 12 bytes (devido a padding)
// Com campos de bits
struct PixelCompacto {
unsigned int x : 10; // supondo resolução máxima 1023
unsigned int y : 10; // 10 bits cada
unsigned int r : 8;
unsigned int g : 8;
unsigned int b : 8;
unsigned int alpha : 8;
}; // total: 52 bits -> 7 bytes (arredondado para 8)
No segundo caso, a estrutura ocupa menos memória (embora o compilador possa adicionar padding para alinhamento). Essa economia é crucial em sistemas com pouca memória, como microcontroladores, ou quando se processa grandes volumes de dados, como em buffers de rede.
No entanto, é importante lembrar que o tamanho real da estrutura pode ser maior que a soma dos bits, devido ao alinhamento da unidade de armazenamento (geralmente 1, 2, 4 ou 8 bytes). O compilador pode inserir preenchimento para manter o alinhamento. Para saber o tamanho exato, use sizeof.
Portabilidade
Campos de bits têm problemas de portabilidade que devem ser considerados. A norma C (C11) define alguns aspectos, mas deixa muitos detalhes para a implementação. Os principais pontos são:
- Ordem de alocação dos bits: A ordem em que os campos são atribuídos aos bits (do bit mais significativo para o menos, ou vice-versa) é definida pela implementação. Isso significa que uma estrutura de campos de bits pode ter uma representação binária diferente em plataformas diferentes.
- Alinhamento: O compilador pode decidir onde colocar os campos na unidade de armazenamento, e pode haver preenchimento (padding) entre campos ou no final da estrutura.
- Tipo base: O tipo base (
intvsunsigned int) pode afetar a interpretação de valores. Por exemplo, um campo de 1 bit com tipointpode ser tratado como sinalizado ou não, dependendo da implementação. - Limitações de tamanho: O número máximo de bits em um campo é limitado pelo tamanho do tipo base. Por exemplo, não se pode declarar um campo de 33 bits em um
unsigned intde 32 bits.
Por causa dessas questões, campos de bits não são recomendados para dados que serão serializados ou transmitidos entre sistemas diferentes, a menos que você use técnicas adicionais, como máscaras e deslocamentos, para garantir uma representação consistente. Em vez disso, eles são frequentemente usados em código específico de uma plataforma, como drivers de dispositivos.
Se você precisar de portabilidade total, é melhor usar operadores bit a bit (como &, |, <<, >>) para manipular bits manualmente, ou usar bibliotecas que abstraem isso.
Casos de uso
Os campos de bits são amplamente utilizados em situações onde o espaço é crítico ou onde se lida diretamente com hardware. Alguns casos comuns:
- Sistemas embarcados: Para controlar registradores de periféricos, onde cada bit tem um significado específico (ex.: registrador de status de um UART).
- Protocolos de rede: Para interpretar cabeçalhos de pacotes, como o cabeçalho IPv4, que contém campos de 4 bits (versão, IHL) e 3 bits (flags).
- Compactação de dados: Para armazenar vários valores pequenos em um único inteiro, como em formatos de arquivo com campos de tamanho fixo.
- Bancos de dados: Para armazenar flags booleanas em um único byte, economizando espaço.
- Jogos e gráficos: Para armazenar atributos de objetos, como tipo, visibilidade, cor, em um único inteiro.
Exemplo de uso em um registrador de hardware:
struct RegistradorStatus {
unsigned int tx_ready : 1;
unsigned int rx_ready : 1;
unsigned int error : 1;
unsigned int : 5; // reservado
};
volatile struct RegistradorStatus *status = (struct RegistradorStatus *)0x40004000;
if (status->rx_ready) {
// receber dado
}
Nesse exemplo, o endereço 0x40004000 é um registrador mapeado em memória, e os campos de bits permitem acessar bits individuais de forma legível.
Boas práticas e observações
Ao usar campos de bits, considere as seguintes boas práticas:
- Use sempre
unsigned intou_Boolpara campos de bits, a menos que você precise explicitamente de um campo sinalizado (o que é raro). - Evite usar campos de bits em dados serializados ou transmitidos, a menos que você controle a ordem dos bits.
- Documente a ordem dos bits esperada, se o código for usado em múltiplas plataformas.
- Prefira operadores bit a bit quando a portabilidade for essencial.
- Verifique o tamanho da estrutura com
sizeofpara garantir que a economia de memória é realmente alcançada.
Referências
- C reference - Bit fields (cppreference.com)
- GNU C Intro and Ref - Bit Fields
- IBM Documentation - Bit fields
- TutorialsPoint - C Bit Fields
- GeeksforGeeks - Bit Fields in C
- Wikipedia - Bit field
Exercícios
-
Explique a diferença entre um campo de bits e um membro de estrutura comum. Dê um exemplo de declaração de um campo de bits de 3 bits para armazenar um valor de 0 a 7.
✓ Resposta: Um campo de bits ocupa um número específico de bits em uma unidade de armazenamento, permitindo economizar memória. Exemplo:struct { unsigned int valor : 3; } s; // valor pode ser 0 a 7 -
Qual é o tamanho em bytes da estrutura a seguir? Justifique.
struct Exemplo { unsigned int a : 4; unsigned int b : 4; };✓ Resposta: A estrutura ocupa 4 bytes (tamanho de umunsigned int) em muitas plataformas, pois os dois campos de 4 bits são armazenados no mesmo inteiro. O compilador pode adicionar preenchimento, mas o tamanho mínimo é o tamanho do tipo base, geralmente 4 bytes. -
Escreva um programa que defina uma estrutura com campos de bits para representar um cartão de ponto: dia (5 bits), mês (4 bits), ano (7 bits) e horas trabalhadas (5 bits). Imprima o tamanho da estrutura e atribua valores a cada campo.
✓ Resposta:#include <stdio.h> struct Ponto { unsigned int dia : 5; unsigned int mes : 4; unsigned int ano : 7; unsigned int horas : 5; }; int main() { struct Ponto p; p.dia = 15; p.mes = 12; p.ano = 2024; // 2024 % 128 = 104, mas cabe em 7 bits (0-127) p.horas = 8; printf("Tamanho: %zu bytes\n", sizeof(p)); printf("Dia: %u, Mês: %u, Ano: %u, Horas: %u\n", p.dia, p.mes, p.ano, p.horas); return 0; } -
Discuta os problemas de portabilidade associados a campos de bits. Como você evitaria esses problemas se precisasse enviar uma estrutura com campos de bits por uma rede?
✓ Resposta: Os problemas incluem: ordem de bits indefinida, alinhamento e preenchimento variáveis, e interpretação de sinal. Para enviar pela rede, é melhor serializar os campos usando operadores bit a bit para montar um inteiro com uma ordem definida, como big-endian, e enviar esse inteiro. -
Considere a seguinte estrutura:
struct Flags { unsigned int a : 1; unsigned int b : 1; unsigned int c : 1; };Qual é o valor de
sizeof(struct Flags)? Explique por que pode variar entre compiladores.✓ Resposta: O tamanho é geralmente 4 bytes (tamanho deunsigned int), mas pode ser 1 byte se o compilador empacotar os bits em um byte, dependendo da arquitetura e das opções de compilação. A norma permite que o compilador escolha a unidade de armazenamento, então o tamanho pode variar.