Saltar para o conteúdo
Topos Uranos

Resumo

Para o processador, um inteiro é uma fileira de bits, e C++ oferece operadores que atuam sobre essa fileira coluna por coluna: &, |, ^ e ~, e os deslocamentos << e >>. Esta aula ensina a calculá-los à mão, a usá-los para guardar várias flags num só inteiro com a técnica das máscaras, e a reconhecer os deslocamentos que o padrão não define. Explica depois por que 2 ^ 3 vale 1 e não 8, e por que os operadores bit a bit exigem parênteses. A segunda metade apresenta a biblioteca matemática: as funções de <cmath>, as constantes de <numbers>, o máximo divisor comum e o mínimo múltiplo comum de <numeric> e as funções de bits de <bit>, com um princípio que vale em qualquer linguagem: o que a biblioteca oferece correto e testado não se reescreve.

Objetivos de aprendizagem

  1. Calcular à mão o resultado de &, |, ^, ~ e dos deslocamentos sobre inteiros pequenos.
  2. Distinguir os deslocamentos definidos (também os de negativos, desde C++20) dos que são comportamento indefinido, e prever o resultado dos primeiros.
  3. Usar máscaras para consultar, ligar, desligar e alternar um bit.
  4. Explicar por que 2 ^ 3 vale 1 e não 8, e escolher entre std::pow e um deslocamento conforme o caso.
  5. Usar funções de <cmath> (std::sqrt, std::pow, std::abs, std::hypot, as trigonométricas) e as constantes de <numbers>, como std::numbers::pi, em fórmulas.

Operadores bit a bit

A aula Os inteiros e sua representação mostrou que um int é uma fileira de bits, 32 nos sistemas do curso, e que seu valor é a soma dos pesos das posições onde há um 1. Até aqui, porém, o programa só tratou o inteiro como um número: somou-o, multiplicou-o e dividiu-o. Ora, a mesma fileira admite outra leitura, como uma coleção de 32 respostas de sim ou não, e para essa leitura C++ tem quatro operadores que trabalham sobre os bits um a um, sem transporte entre posições.

Os bits são numerados a partir da direita, começando por 0: o bit kk é o de peso 2k2^k. Assim, em 1212, que em binário é 1100, estão ligados os bits 2 e 3, e desligados os bits 0 e 1.

Os operadores são estes. O E bit a bit, a & b, tem um 1 em cada posição onde a e b têm ambos um 1. O OU bit a bit, a | b, tem um 1 onde ao menos um dos dois o tem. O OU exclusivo, a ^ b, tem um 1 onde exatamente um dos dois o tem, isto é, onde os bits diferem. A negação bit a bit, ~a, tem um só operando e inverte cada bit. As tabelas dos três primeiros, para um par de bits pp e qq, são as da conjunção, da disjunção e da disjunção exclusiva:

pqp&qp∣qp^q11110100110101100000\begin{array}{cc|ccc} p & q & p \mathbin{\&} q & p \mid q & p \mathbin{\hat{}} q \\ \hline 1 & 1 & 1 & 1 & 0 \\ 1 & 0 & 0 & 1 & 1 \\ 0 & 1 & 0 & 1 & 1 \\ 0 & 0 & 0 & 0 & 0 \end{array}

Quem tiver estudado o curso Lógica Matemática I reconhecerá aqui os conectivos proposicionais, com 1 por verdadeiro e 0 por falso: um operador bit a bit não é outra coisa senão um conectivo aplicado a todas as colunas ao mesmo tempo. Daí se segue o método de cálculo à mão: escrevem-se os dois números em binário, um sobre o outro, alinhados pela direita, e aplica-se a tabela em cada coluna, sem se ocupar das vizinhas.

ExemploDoze e dez, coluna por coluna

Os números 12=12 = 1100 e 10=10 = 1010 têm a propriedade de que suas quatro colunas percorrem as quatro combinações de dois bits; por isso servem para ver as três tabelas ao mesmo tempo.

Demonstração

  1. Os dados: 1100 é 12 e 1010 é 10. As colunas, da esquerda para a direita, têm os pares (1,1)(1, 1), (1,0)(1, 0), (0,1)(0, 1) e (0,0)(0, 0).

  2. Primeira coluna de 12 & 10: ambos os bits são 1, e o E dá 1.

  3. Segunda coluna: 1 e 0. Basta um 0 para que o E dê 0.

  4. Terceira coluna: 0 e 1; o E dá 0 outra vez.

  5. Quarta coluna: 0 e 0 dão 0. A fileira resultado, 1000, é 8: 12 & 10 vale 8.

  6. O OU liga cada coluna que tenha ao menos um 1: 1110, isto é, 12 | 10 vale 14.

  7. O OU exclusivo liga só as colunas cujos bits diferem: 0110, e 12 ^ 10 vale 6.

O programa seguinte comprova a figura. A especificação de formato {:04b}, apresentada na aula sobre os inteiros, escreve um valor em binário com quatro algarismos, completados com zeros.

Programa em C++
#include <print>​int main(){    const int a{0b1100};    const int b{0b1010};    std::println("a     = {:04b} = {}", a, a);    std::println("b     = {:04b} = {}", b, b);    std::println("a & b = {:04b} = {}", a & b, a & b);    std::println("a | b = {:04b} = {}", a | b, a | b);    std::println("a ^ b = {:04b} = {}", a ^ b, a ^ b);    std::println("~a    = {}", ~a);}
Saída
a     = 1100 = 12b     = 1010 = 10a & b = 1000 = 8a | b = 1110 = 14a ^ b = 0110 = 6~a    = -13

A última linha merece uma explicação. A negação inverte os 32 bits de a, não só os quatro que se escrevem: os 28 zeros da esquerda tornam-se uns, e um int que começa com 1 é negativo em complemento de dois. Ora, em complemento de dois o oposto de um número obtém-se invertendo seus bits e somando 1, isto é, −x=∼x+1-x = {\sim}x + 1; portanto, ∼x=−x−1{\sim}x = -x - 1, e ~12 vale −13-13. A negação bit a bit sobre um inteiro com sinal dá, então, um número que raramente interessa como número: interessa como configuração, e por isso os bits se manipulam de preferência em tipos sem sinal, como se verá na seção das máscaras.

Uma advertência sobre os tipos pequenos. Antes de operar, C++ converte para int todo operando de um tipo inteiro mais estreito, como std::uint8_t; é a promoção que a aula Aritmética descreveu para as operações aritméticas, e vale também aqui. Daí se segue que ~flags, com flags do tipo std::uint8_t e valor 5, não é o byte 11111010, mas o int −6-6, cujos 32 bits incluem 24 uns acrescentados à esquerda; só ao guardá-lo de novo num std::uint8_t ele é recortado a oito bits e se recupera o byte esperado, 250.

Não se devem confundir os operadores bit a bit com os operadores lógicos da aula Relações e lógica. 6 && 3 é true: ambos os operandos são diferentes de zero, e o resultado é um bool. 6 & 3, por sua vez, é um inteiro, o que formam as colunas onde ambos têm um 1. Além disso, && é avaliado em curto-circuito e & avalia sempre ambos os operandos.