Manipulação ao nível do bit na Linguagem C

É sabido que um computador trabalha em modo binário, armazenando e manipulando bits, isto é, zeros e uns. Este artigo procura resumir as metodologias mais comuns para uso e manipulação de bits através da linguagem C.

Base binária, octal e hexadecimal

A designação bit identifica um valor da base binária. Como o nome sugere, a base binária é composta por dois valores distintos, representados por zero e um, daí também se designar por base dois. Assim, um bit pode assumir um desses dois valores, sendo muitas vezes empregue para representar um estado ativo (bit com o valor a 1) ou inativo (bit com valor a 0).

Vários bits podem ser agrupados para formar valores com maior amplitude. Concretamente, sempre que se acrescenta um bit, está-se a duplicar o número de valores passíveis de serem representados pelo conjunto de bits. Por exemplo, com um bit consegue-se representar dois estados (0 e 1). Com dois bits já é possível representar quatro estados (00, 01, 10 e 11) e com três bits oito estados (000, 001, 010, 011, 100, 101, 110 e 111). De uma forma geral, n bits permitem a representação de 2n estados diferentes. Por exemplo, 16 bits permitem a representação de 216 valores distintos, isto é, 65536. É essa a razão porque um inteiro de 16 bits sem sinal (i.e., unsigned) pode representar valores entre 0 e 65535. Outro exemplo é o do octeto, que designa um conjunto de oito bits e que pode representar 28, i.e., 256 valores inteiros, seja entre -128 e 127 (octeto com sinal) ou entre 0 e 255 (octeto sem sinal). O termo byte é frequentemente empregue para designar um octeto.

A representação binária é usualmente pouco conveniente para o ser humano que facilmente se perde na contagem e localização dos bits, especialmente se existir um número elevado de bits. Deste modo, os programadores usam frequentemente a base octal e a base hexadecimal como alternativa à representação binária. Estas duas bases são empregues por serem mais compactas e pela facilidade com que se consegue converter de uma representação para a representação binária e vice-versa.

A base octal tem um conjunto de oito símbolos para a representação de uma dada quantidade. Os símbolos são os dígitos compreendidos entre 0 e 7. No caso da linguagem C (e de muitas outras), um valor em base octal é representado com um zero à esquerda. Assim, o valor 0123 numa listagem de código C identifica o valor octal 123 e não o valor decimal 123. Na realidade, o valor 0123 corresponde ao valor 83 em base decimal. A conversão de octal para decimal pode ser feita somando-se as parcelas resultantes da multiplicação de 3 por 80, de 2 por 81 e de 1 por 82 obtendo-se o valor 3×1+2×8+8×8=83. Note-se que o uso do zero à esquerda para indicar que uma constante inteira está em base octal pode confundir o programador menos atento que poderá pensar tratar-se de uma constante em base 10 com um insignificante zero à esquerda.

Conforme já referido, o maior interesse da base octal é a facilidade de conversão para a respetiva representação binária e vice-versa. De facto, por ser composta por 8 símbolos, cada símbolo octal é representado em binário por três bits, pois três bits permitem gerar 8 valores distintos (23 = 8). Por exemplo, o símbolo octal 3 é representado em binário por 011, o símbolo 6 por 110. A Tabela 1 mostra a conversão entre octal e binário para os oito símbolos da base octal. Voltando ao exemplo anterior, o valor octal 123 (0123 na linguagem C) tem a representação binária 001.010.011, correspondendo à substituição dos símbolos da base octal pelos respetivos valores binários (os pontos empregues na representação binária destinam-se somente a simplificar a leitura). Importa referir que algumas linguagens de programação já suportam representação binária, usando o prefixo 0b antes da quantidade numérica. É o caso da linguagem Java (somente a partir da versão 7), na qual o valor binário correspondendo ao valor octal 0123 poderia ser representado como 0b001010011.

Tabela 1: Mapeamento entre base octal e base binária
Octal Binário
0 000
1 001
2 010
3 011
4 100
5 101
6 110
7 111

De modo similar à base octal, uma representação em base hexadecimal é facilmente convertida numa representação binária e vice-versa. A base hexadecimal assenta num conjunto de 16 símbolos, usando os algarismos 0 a 9 para a representação dos 10 primeiros símbolos e as letras de A a F para os restantes seis símbolos. Nas linguagens de programação, as constantes em base hexadecimal são identificadas pelo prefixo 0x. Assim, retomando o exemplo anterior, 0x123 representa uma quantidade em base 16. A conversão de uma valor hexadecimal para base 10 consiste em somar as parcelas resultantes da multiplicação do símbolo mais à direita por 160 (símbolo designado como o menos significativo), da multiplicação do segundo símbolo mais à direita por 161, da multiplicação do terceiro símbolo mais à direita por 162 e assim sucessivamente. Para o caso do valor 0x123, obtém-se 3×160 + 2×161+ 1×162, isto é, 3+32+256, ou seja o valor decimal 291. Usando uma notação frequentemente empregue, pode dizer-se que (123)16 corresponde ao valor (291)10.

A conversão de um valor hexadecimal para a representação binária equivalente processa-se de forma similar à conversão de um valor octal para binário, exceto que cada símbolo hexadecimal deve ser mapeado para um valor de 4 bits de acordo com a Tabela 2. O uso de 4 bits por símbolo decorre do facto que são necessário 4 bits para representar todos os 16 símbolos empregues na base hexadecimal (24=16). Aplicando-se a metodologia de conversão hexadecimal para binário ao exemplo 0x123, obtém-se a seguinte representação em binário: 0001.0010.0011.

Tabela 2: Mapeamento entre hexadecimal e base binária
Hexadecimal Binário
0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001
A 1010
B 1011
C 1100
D 1101
E 1110
F 1111

A maior frequência de uso na programação da representação hexadecimal em relação à representação octal deriva do facto de um valor hexadecimal apresentar um tamanho que é sempre múltiplo de 4 bits. Essa característica possibilita que facilmente possa ser encontrado um valor hexadecimal com o mesmo número de bits de um tipo de dados inteiro. Por exemplo, para o caso de se pretender um valor inteiro com 16 bits, apenas é necessário garantir que a representação hexadecimal tenha 4 símbolos. Similarmente, para um valor de 32 bits, sabe-se que é apropriado um valor hexadecimal com 8 símbolos e assim sucessivamente. Adicionalmente, o formato hexadecimal é empregue para a representação de endereços, dado os endereços terem geralmente um número de bits que é uma potência de dois (8, 16, 32, 64, etc.). Deste modo, não surpreende que a linguagem C disponibilize através da função printf e do respetivo operador de formatação %x, a representação de um determinado valor em formato hexadecimal. Note-se que em alternativa ao operador %x, pode ser empregue o operador %X (maiúscula) que apresenta o mesmo resultado, exceto que usa maiúsculas para representar os símbolos hexadecimais entre A e F.

Especificação de campos de bits em estruturas

A linguagem C possibilita a declaração de campos binários em estruturas do tipo struct. Assim, é possível declarar um ou mais elementos de uma struct como sendo um conjunto de bits. A manipulação do conjunto de bits assim definidos faz-se através do campo da struct. Considere-se o exemplo da struct exemplo1 apresentado na Listagem 1, na qual estão declarados os campos campo01 e campo02, respetivamente com dois e quatro bits. O uso de um campo de bits é efetuado da mesma forma que qualquer outro campo da estrutura, especificando-se o nome do campo. No caso da Listagem 1 são atribuídos os valores 1 (em decimal, correspondendo a 01 em binário) e 0xA (em hexadecimal, correspondendo a 1010 em binário), respetivamente, aos campos campo01 e campo02.

/* Exemplo: "bit_fields.c" */
#include <stdio.h>
typedef struct exemplo1{
   int campo_bit01:2;
   unsigned int campo_bit02:4;
   float valor_float;
}exemplo1_t;

exemplo1_t exemplo;
exemplo1.campo01 = 1;
exemplo1.campo02 = 0xA;
printf("campo01=%d\n", exemplo1.campo01);
printf("campo02=%d\n", exemplo1.campo02);
Listagem 1: exemplo bit_fields.c

Importa notar que um elemento especificado como campo de bits deve ser obrigatoriamente declarado como sendo do tipo int (ou equivalentemente do tipo signed int) ou do tipo unsigned int. O número de bits definido para o campo condiciona os valores que lá podem ser armazenado. Assim, para o caso do campo01, os dois bits do campo permitem armazenar um dos conjuntos binários 00, 01, 10 ou 11. Adicionalmente, dado que campo01 é declarado com int, isto é, inteiro com sinal, os valores inteiros que o campo pode armazenar são o -2, o -1, 0 e 1. Por sua vez, o elemento campo02 tem espaço para quatro bits, pelo que lhe pode ser atribuído um valor hexadecimal desde que tenha somente um dígito, como é o caso do valor 0xA empregue na Listagem 1.

Bits e variáveis inteiras

Na linguagem C, o acesso ao nível do bits não está limitado a campos de bits definidos em structs. De facto, é possível efetuar operações envolvendo operadores binários em variáveis do tipo inteiro, sejam elas int, short, long ou mesmo char, independentemente de ser considerado o sinal ou não (signed/unsigned). A principal diferença entre o uso de um campo de bits e o uso de uma variável inteira reside no facto que uma operação binária numa variável inteira envolve todos os bits da variável, ao passo que num campo de bits, apenas são afetados os bits do campo de bits. Assim, quando se efetua uma operação binária envolvendo, por exemplo, uma variável inteira sem sinal com 32 bits, é necessário considerar os efeitos da operação sobre os 32 bits que compõem a variável. É pois importante, quando se faz uso de uma variável inteira, ter em conta o número de bits da variável, algo que pode ser determinado multiplicando o resultado devolvido pelo operador sizeof por 8, dado que esse operador devolve o tamanho em octetos (bytes) da variável ou do tipo de dados que lhe é passado como parâmetro (Listagem 2). A norma C99 introduziu tipos de dados com tamanho explicitado, como é o caso do tipo int8_t que corresponde a um valor inteiro com sinal de 8 bits (i.e., um octeto) ou o uint16_t que tem 16 bits para guardar valores inteiros sem sinal (Open-STD, 2003). A norma C99 especifica ainda que os tipos inteiros explicitados se encontram definidos no ficheiro <inttypes.h>.

// Exemplo: 
int var_a;
printf("nº bits 'int'=%d\n",sizeof(var_a)*8);
printf("nº bits 'short'=%d\n", sizeof(short)*8);
Listagem 2: exemplo sizeof.c

Publicado na edição 50 (PDF) da Revista PROGRAMAR.