Manipulação de Bits e Máscaras

[393] Manipulação de Bits e Máscaras

Trocar & por && é o erro sutil que ronda esta aula: um pesa condições, o outro opera bit a bit. Do resto ficam três idiomas para guardar de cor — ligar, desligar e alternar um bit com máscara —, a equivalência entre deslocar e multiplicar por potência de dois, e a regra de usar sempre tipo unsigned.
Linguagem C

10 min de leitura

Até agora, tratamos os números como quantidades: um int valia 42, um char guardava a letra 'A'. Hoje descemos ao nível mais fundamental de todos e olhamos esses valores pelo que realmente são na memória — sequências de bits, zeros e uns. O C, fiel à sua natureza de linguagem próxima da máquina, oferece operadores para manipular esses bits diretamente. É uma habilidade que parece esotérica, mas que aparece em toda parte: drivers, protocolos de rede, compressão, criptografia, e otimizações que economizam memória preciosa. Bem-vindo à camada mais baixa que o C nos deixa tocar.

Lembrando que tudo são bits

Um unsigned char ocupa 8 bits. O número 42, nesse byte, é a sequência 00101010. Cada posição vale uma potência de 2: da direita para a esquerda, 1, 2, 4, 8, 16, 32, 64, 128. Somando as posições onde há um 1 (2 + 8 + 32 = 42), obtemos o valor. Manipulação de bits é operar diretamente sobre essas posições — ligar, desligar, testar e deslocar bits individuais. Para isso, o C oferece seis operadores dedicados.

Os operadores bit a bit

São seis, e cada um faz uma operação lógica sobre os bits correspondentes de dois números (ou, no caso do NOT, de um só):

&   AND bit a bit      — 1 se AMBOS os bits são 1
|   OR bit a bit       — 1 se PELO MENOS UM bit é 1
^   XOR bit a bit      — 1 se os bits são DIFERENTES
~   NOT bit a bit      — inverte cada bit (unário)
<<  deslocamento à esquerda — empurra bits para a esquerda
>>  deslocamento à direita  — empurra bits para a direita

Um cuidado importante desde já: não confunda os operadores bit a bit & e | com os operadores lógicos && e || que vimos na aula de controle de fluxo. && avalia condições verdadeiro/falso; & opera sobre os bits individuais dos números. São coisas diferentes, e trocá-los é um erro sutil.

AND, OR e XOR na prática

Vejamos os três operadores binários em ação, bit a bit:

#include <stdio.h>

int main(void) {
    unsigned char a = 12; // 00001100
    unsigned char b = 10; // 00001010

    printf("a & b = %d\n", a & b); // 00001000 = 8  (só onde ambos são 1)
    printf("a | b = %d\n", a | b); // 00001110 = 14 (onde pelo menos um é 1)
    printf("a ^ b = %d\n", a ^ b); // 00000110 = 6  (onde diferem)
    printf("~a    = %d\n", (unsigned char)~a); // 11110011 = 243 (inverte tudo)
    return 0;
}

Compare os bits para entender cada resultado. Em a & b, apenas a posição onde ambos têm 1 (o bit de valor 8) sobrevive. Em a | b, qualquer posição com pelo menos um 1 fica ligada. Em a ^ b (XOR), só ficam ligadas as posições onde os bits diferem. Cada operador tem seus usos característicos, que veremos com as máscaras.

Deslocamento: multiplicar e dividir por potências de 2

Os operadores de deslocamento movem todos os bits para a esquerda ou direita:

#include <stdio.h>

int main(void) {
    unsigned char x = 3; // 00000011

    printf("x << 1 = %d\n", x << 1); // 00000110 = 6  (dobra)
    printf("x << 2 = %d\n", x << 2); // 00001100 = 12 (quadruplica)
    printf("x >> 1 = %d\n", 12 >> 1); // 00000110 = 6 (metade)
    return 0;
}

Há uma equivalência elegante aqui: deslocar n bits à esquerda multiplica por 2ⁿ, e deslocar à direita divide por 2ⁿ (descartando o resto). x << 1 dobra; x << 3 multiplica por 8; x >> 1 divide por 2. Historicamente, deslocamentos eram usados como multiplicação/divisão rápida — hoje os compiladores fazem essa otimização sozinhos, mas o deslocamento continua essencial para construir e extrair campos de bits, como veremos.

Máscaras de bits: o padrão fundamental

O uso mais prático da manipulação de bits são as máscaras — valores construídos para isolar, ligar ou desligar bits específicos. As três operações essenciais formam um vocabulário que você usará sempre:

Para testar se um bit específico está ligado, use & com uma máscara que tem 1 só naquela posição:

#include <stdio.h>

int main(void) {
    unsigned char flags = 0b00000101; // bits 0 e 2 ligados (C permite literais binários em GCC)

    // testa o bit 2 (máscara 00000100, ou seja, 1 << 2)
    if (flags & (1 << 2)) {
        printf("Bit 2 está ligado.\n"); // sim, está
    }
    // testa o bit 1 (máscara 00000010)
    if (flags & (1 << 1)) {
        printf("Bit 1 está ligado.\n");
    } else {
        printf("Bit 1 está desligado.\n"); // este caso
    }
    return 0;
}

A máscara 1 << 2 produz 00000100 — um 1 na posição 2. O AND (flags & máscara) resulta em não-zero apenas se aquele bit estiver ligado em flags. Esse é o idioma universal para "verificar se a flag X está ativa".

Para ligar um bit, use | com a máscara; para desligar, use & com o inverso (~) da máscara:

#include <stdio.h>

int main(void) {
    unsigned char flags = 0;

    flags |= (1 << 0);  // liga o bit 0 -> 00000001
    flags |= (1 << 3);  // liga o bit 3 -> 00001001
    printf("Depois de ligar: %d\n", flags); // 9

    flags &= ~(1 << 0); // desliga o bit 0 -> 00001000
    printf("Depois de desligar: %d\n", flags); // 8

    flags ^= (1 << 3);  // ALTERNA o bit 3 (liga se desligado, desliga se ligado)
    printf("Depois de alternar: %d\n", flags); // 0
    return 0;
}

Grave esse trio de idiomas, pois é o coração da manipulação de bits: flags |= (1 << n) liga o bit n; flags &= ~(1 << n) desliga o bit n; flags ^= (1 << n) alterna o bit n. Eles usam os operadores compostos (|=, &=, ^=) que combinam a operação com a atribuição, exatamente como += faz com a soma.

Por que isso importa: flags compactadas

O uso mais comum dessas técnicas é guardar múltiplas opções booleanas num único inteiro, economizando memória e permitindo combiná-las. Em vez de oito variáveis bool separadas, um único byte pode representar oito flags independentes:

#include <stdio.h>

// cada permissão é um bit distinto
#define LER     (1 << 0)  // 00000001
#define ESCREVER (1 << 1) // 00000010
#define EXECUTAR (1 << 2) // 00000100

int main(void) {
    // combina permissões com OR
    unsigned char permissoes = LER | ESCREVER; // 00000011

    printf("Pode ler?     %s\n", (permissoes & LER)     ? "sim" : "não"); // sim
    printf("Pode escrever?%s\n", (permissoes & ESCREVER) ? "sim" : "não"); // sim
    printf("Pode executar?%s\n", (permissoes & EXECUTAR) ? "sim" : "não"); // não
    return 0;
}

Se esse padrão parece familiar, é porque é exatamente como o sistema de permissões de arquivos do Unix funciona (aqueles rwx do chmod) e como incontáveis APIs recebem opções combináveis. Definir cada opção como um bit distinto (1 << 0, 1 << 1, 1 << 2...) permite combiná-las com |, testá-las com &, e guardá-las de forma extremamente compacta. É a manipulação de bits resolvendo um problema real de design de forma elegante.

Um cuidado importante: use tipos unsigned

Uma recomendação de segurança: ao manipular bits, prefira tipos unsigned (unsigned int, unsigned char). Com tipos com sinal, o deslocamento à direita e o comportamento do bit mais significativo (o bit de sinal) podem produzir resultados dependentes de implementação ou até comportamento indefinido. Os tipos unsigned têm comportamento de bits bem definido e previsível pela norma. Sempre que seu foco for manipular bits — em vez de fazer aritmética com sinal — trabalhe com tipos sem sinal. É a mesma disciplina defensiva de sempre, aplicada a este nível mais baixo.

O que vem a seguir

Hoje mergulhamos na camada mais próxima da máquina que o C nos permite tocar: os operadores bit a bit (&, |, ^, ~, <<, >>) e o padrão das máscaras para testar, ligar, desligar e alternar bits individuais — culminando nas flags compactadas que resolvem problemas reais de design. Na próxima aula, aplicamos esse conhecimento a um recurso do C que o formaliza: os campos de bits dentro de structs, que permitem declarar membros com um número exato de bits, dando nome e estrutura ao empacotamento que hoje fizemos na mão.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Dado unsigned char x = 20; (00010100) e unsigned char y = 6; (00000110), calcule à mão e depois confirme no programa os resultados de x & y, x | y e x ^ y.

Ver resposta

✓ Resposta: À mão: x = 00010100, y = 00000110. - x & y = 00000100 = 4 (só a posição onde ambos têm 1) - x | y = 00010110 = 22 (posições com pelo menos um 1) - x ^ y = 00010010 = 18 (posições onde diferem)

#include <stdio.h>
int main(void) {
    unsigned char x = 20, y = 6;
    printf("%d %d %d\n", x & y, x | y, x ^ y); // 4 22 18
    return 0;
}

Exercício 2

Escreva uma função int bit_ligado(unsigned int valor, int posicao) que retorne 1 se o bit na posição indicada estiver ligado, e 0 caso contrário. Teste com alguns valores e posições.

Ver resposta

✓ Resposta:

#include <stdio.h>
int bit_ligado(unsigned int valor, int posicao) {
    return (valor & (1u << posicao)) != 0;
}
int main(void) {
    printf("%d\n", bit_ligado(5, 0)); // 1 (5 = 101, bit 0 ligado)
    printf("%d\n", bit_ligado(5, 1)); // 0 (bit 1 desligado)
    printf("%d\n", bit_ligado(5, 2)); // 1 (bit 2 ligado)
    return 0;
}

A máscara 1u << posicao isola o bit desejado; o AND resulta não-zero se ele estiver ligado. O != 0 normaliza para 0 ou 1.

Exercício 3

Escreva três funções: ligar(unsigned char *f, int n), desligar(unsigned char *f, int n) e alternar(unsigned char *f, int n), que respectivamente ligam, desligam e alternam o bit n de uma variável de flags passada por ponteiro. Teste a sequência de operações.

Ver resposta

✓ Resposta:

#include <stdio.h>
void ligar(unsigned char *f, int n)    { *f |=  (1 << n); }
void desligar(unsigned char *f, int n)  { *f &= ~(1 << n); }
void alternar(unsigned char *f, int n)  { *f ^=  (1 << n); }

int main(void) {
    unsigned char flags = 0;
    ligar(&flags, 0);    printf("%d\n", flags); // 1
    ligar(&flags, 2);    printf("%d\n", flags); // 5
    desligar(&flags, 0); printf("%d\n", flags); // 4
    alternar(&flags, 2); printf("%d\n", flags); // 0
    return 0;
}

Passamos &flags para que as funções modifiquem o original via ponteiro — a lição da aula Ponteiros: O que É um Endereço de Memória aplicada a bits.

Exercício 4

Use deslocamento de bits para multiplicar um número por 8 e outro para dividir por 4, sem usar * nem /. Confirme que os resultados batem com a multiplicação/divisão comuns.

Ver resposta

✓ Resposta:

#include <stdio.h>
int main(void) {
    int a = 5;
    printf("%d\n", a << 3);   // 5 * 8 = 40
    int b = 40;
    printf("%d\n", b >> 2);   // 40 / 4 = 10
    return 0;
}

Deslocar 3 bits à esquerda multiplica por 2³ = 8; deslocar 2 à direita divide por 2² = 4. Os resultados (40 e 10) coincidem com 5 * 8 e 40 / 4.

Exercício 5

Defina três macros de permissão (LER, ESCREVER, EXECUTAR) como bits distintos. Escreva um programa que combine LER | EXECUTAR numa variável e depois teste, com &, quais das três permissões estão ativas, imprimindo "sim"/"não" para cada.

Ver resposta

✓ Resposta:

#include <stdio.h>
#define LER      (1 << 0)
#define ESCREVER (1 << 1)
#define EXECUTAR (1 << 2)

int main(void) {
    unsigned char p = LER | EXECUTAR; // 00000101

    printf("Ler:      %s\n", (p & LER)      ? "sim" : "não"); // sim
    printf("Escrever: %s\n", (p & ESCREVER) ? "sim" : "não"); // não
    printf("Executar: %s\n", (p & EXECUTAR) ? "sim" : "não"); // sim
    return 0;
}

Combinamos duas permissões com | e testamos cada uma com &. O padrão reproduz, em miniatura, o esquema de permissões do Unix.

Comentários

Mais em Linguagem C

Capstone: Um Projeto Real do Zero e a Retrospectiva da Jornada
Capstone: Um Projeto Real do Zero e a Retrospectiva da Jornada

Um gerenciador de tarefas de linha de comando, do zero: struct e lista…

Threads com pthreads
Threads com pthreads

contador++ não é uma operação só: são ler, somar e escrever — e duas threads…

Campos de Bits e Estruturas Compactas
Campos de Bits e Estruturas Compactas

Declarar dia : 4 e gravar 20 trunca o valor sem aviso nenhum — dimensionar…