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
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 2.9, Bitwise Operators
- cppreference — Bitwise operators — https://en.cppreference.com/w/c/language/operator_arithmetic
- Modern C, Jens Gustedt — capítulo sobre operações bit a bit e tipos unsigned — https://gustedt.gitlabpages.inria.fr/modern-c/
- Hacker's Delight, Henry Warren — a bíblia dos truques de manipulação de bits
- CERT C — INT13-C, sobre operadores bit a bit em operandos com sinal — https://wiki.sei.cmu.edu/confluence/display/c
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.