No artigo anterior, aprendemos a somar e subtrair números inteiros como fazemos no papel. Porém, construir um circuito eletrônico físico que “pede emprestado” bits vizinhos é complexo e caro. E se eu te dissesse que o computador, na verdade, não faz a subtração?
Para otimizar o hardware, a engenharia da computação desenvolveu um método para transformar subtrações em adições. Em vez de fazer \(A - B\), o computador realiza \(A + (-B)\).
Mas como representamos um número negativo usando apenas zeros e uns? Se só temos dois símbolos, onde colocamos o sinal de “menos”? É aqui que entra o conceito de Complemento de Dois, a base fundamental da aritmética de números inteiros em processadores modernos.
A Ideia do Complemento #
Antes de pensar em bits, vamos analisar o problema na nossa base decimal comum.
Imagine uma calculadora muito simples que possui um display de apenas 4 dígitos.
O maior número que ela consegue mostrar é 9999.
Se somarmos 1 a esse valor (9999 + 1), o resultado matemático seria 10000.
Mas como o display só tem espaço para 4 números, o 1 da esquerda desaparece
(transborda além do limite do display), e o visor mostra apenas 0000.
Agora, imagine o caminho inverso. Se o visor mostra 0000 e tentamos subtrair
1, a calculadora faz o “giro” contrário e exibe 9999.
Nesse sistema limitado, o número 9999 se comporta matematicamente como se
fosse o -1.
Vamos testar? Se somarmos 5 com 9999 (nosso “-1 fake”):
Descartando o primeiro dígito que não cabe no display, sobra 0004. Ou seja,
\(5 + (-1) = 4\). A conta funcionou perfeitamente usando apenas soma!
Isso é o Complemento à Base: usamos os números “grandes” do topo da contagem para representar os negativos.
Vamos agora formalizar essa ideia.
O complemento de um número \(N\) em uma dada base \(b\) é igual à diferença entre o número e a próxima potência de \(b\).
Complemento à base 10
O complemento à base 10 de um número \(N\) é dado por \(10^d - N\), onde \(d\) é o número de dígitos de \(N\). Imagine que queremos representar o equivalente a “-734” em um sistema de 4 dígitos.
$$ \begin{array}{r r r r} 1 & 0 & 0 & 0\\ -{} & 7 & 3 & 4\\ \hline &2 & 6 & 6 \end{array} $$O número 266 é o “complemento” de 734. Somar 266 terá o mesmo efeito que subtrair 734 (ignorando o estouro, ou seja, o “vai-um” que ultrapassa o limite do display). Vamos visualizar isso um pouco mais adiante neste artigo. Antes, precisamos entender como os números negativos são representados em binário.
Números com Sinal em Binário #
No computador, temos uma quantidade fixa de bits (geralmente 8, 16, 32 ou 64). Para diferenciar positivos de negativos, usamos o Bit Mais Significativo (o bit mais à esquerda, chamado de MSB, do inglês “Most Significant Bit”) como o Bit de Sinal.
- 0 no início indica número Positivo.
- 1 no início indica número Negativo.
Por exemplo, em um sistema de 8 bits:
00000101representa+5.11111011representa-5(em complemento de dois).
Por que 11111011 é -5? Vamos descobrir. Usando a notação posicional:
Interpretando números negativos em complemento de 2
Vamos interpretar o número binário 11111011 como um inteiro com sinal (8 bits).
Expandindo:
Calculando os valores:
$$ 128 + 64 + 32 + 16 + 8 + 0 + 2 + 1 = 251 $$Como o bit mais à esquerda é 1, sabemos que é um número negativo. Para encontrar seu valor real, subtraímos \(256\) (que é \(2^8\), a próxima potência de 2):
$$ 251 - 256 = -5 $$
Portanto, 11111011 representa -5 em complemento de dois.
Esse método funciona para qualquer número de bits.
Uma outra forma, é considerar o notação posicional com o bit de sinal negativo:
$$ -1 \times 2^7 + 1 \times 2^6 + 1 \times 2^5 + 1 \times 2^4 + 1 \times 2^3 + 0 \times 2^2 + 1 \times 2^1 + 1 \times 2^0 $$Calculando os valores:
$$ -128 + 64 + 32 + 16 + 8 + 0 + 2 + 1 = -5 $$Ambas as formas levam ao mesmo resultado.
Complemento de Dois (Base 2) #
O complemento à base 2 de um número \(N\) é matematicamente dado por \(2^d - N\).
Complemento à base 2 (Cálculo Matemático Formal)
Vamos encontrar o negativo de \(0101_2\) (5 decimal) em um sistema de 4 bits. A conta é \(2^4 - 5\), ou seja, \(10000_2 - 0101_2\).
Lembre-se: no algoritmo formal já apresentado nessa série de artigos, quando o dígito de cima é menor, somamos a base (2) a ele e adicionamos 1 ao dígito de baixo da próxima coluna.
$$ \begin{array}{r r r r r r} & 1 & 0^{\scriptscriptstyle+2} & 0^{\scriptscriptstyle+2} & 0^{\scriptscriptstyle+2} & 0^{\scriptscriptstyle+2} \\ -& 0^{\scriptscriptstyle+1} & 0^{\scriptscriptstyle+1} & 1^{\scriptscriptstyle+1} & 0^{\scriptscriptstyle+1} & 1 \\ \hline & 0 & 1 & 0 & 1 & 1 \end{array} $$Passo a passo:
- Col 0: \(0 < 1\). Somamos a base ao topo (\(0+2=2\)). \(2-1=\mathbf{1}\). Compensamos (+1) na próx.
- Col 1: Topo \(0\), Baixo \(0+1=1\). \(0 < 1\). Somamos base (\(2\)). \(2-1=\mathbf{1}\). Compensamos.
- Col 2: Topo \(0\), Baixo \(1+1=2\). \(0 < 2\). Somamos base (\(2\)). \(2-2=\mathbf{0}\). Compensamos.
- Col 3: Topo \(0\), Baixo \(0+1=1\). \(0 < 1\). Somamos base (\(2\)). \(2-1=\mathbf{1}\). Compensamos.
- Col 4: Topo \(1\), Baixo \(0+1=1\). \(1-1=\mathbf{0}\).
Resultado: \(1011_2\) (os 4 bits úteis).
Fazer essa subtração é trabalhoso. Felizmente, existe um algoritmo muito mais simples usado pelo hardware e por programadores: a técnica do “Inverte e Soma 1”.
Técnica: Inverte e Soma 1 #
Esta técnica se baseia no Complemento de 1 (apenas inverter os bits) e depois somar 1 para chegar ao Complemento de 2.
Vamos encontrar o complemento de 2 do número 5 (0101) em um sistema de 4 bits:
- Pegue o número positivo:
0101 - Inverta todos os bits (0 vira 1, 1 vira 0):
1010 - Some 1 ao resultado.
Complemento à base 2 (Inverte e Soma 1)
Após inverter os bits de 0101, obtivemos 1010. Agora, o trabalho é apenas somar 1.
O resultado 1011 representa o -5 em Complemento de 2.
Por que somar 1 funciona? #
Você pode estar se perguntando: por que inverter os bits e somar 1 resulta exatamente no complemento matemático (\(2^N - N\))?
A explicação está na relação entre uma sequência cheia de “uns” e a próxima potência de 2:
- Inverter os bits é matematicamente equivalente a subtrair o número de uma sequência composta apenas por uns (isso é chamado de Complemento de 1).
- Exemplo em 4 bits: Inverter \(x\) é igual a \(1111_2 - x\).
- A definição formal do Complemento de 2 é subtrair da próxima potência da base (\(10000_2\)).
- Ou seja: \(10000_2 - x\).
Como sabemos que \(10000_2\) é exatamente \(1111_2 + 1\), podemos substituir na fórmula:
$$ \underbrace{(10000_2 - x)}_{\text{Definição Formal}} = \underbrace{(1111_2 + 1) - x}_{\text{Substituição}} = \underbrace{(1111_2 - x)}_{\text{Bits Invertidos}} + 1 $$Resumindo: inverter os bits nos leva “quase” lá (até o valor máximo da base menos 1). Adicionar 1 completa a diferença para chegar à potência de 2 correta. É por isso que o algoritmo funciona perfeitamente.
Regra Prática (O Atalho Visual) #
Para humanos, existe um jeito ainda mais rápido de fazer isso “de cabeça”, sem contas:
Para se obter diretamente o complemento a 2 de um número basta percorrer o número da direita para a esquerda, repetindo-se os dígitos zeros até encontrar o primeiro dígito 1, o qual deve ser mantido. A partir daí, todos os dígitos à esquerda desse primeiro 1 deverão ser invertidos.
Exemplos de aplicação da regra:
-
Número: \(011\mathbf{1}00\)
- Mantém o final
100. - Inverte o começo
011\(\to\)100. - Resultado: \(100\mathbf{1}00\)
- Mantém o final
-
Exemplos adicionais:
- \(1010 \to 0110\)
- \(11001 \to 00111\)
- \(111000 \to 001000\)
- \(1100110 \to 0011010\)
O Truque: Subtração via Adição #
Agora que sabemos converter um número para negativo (complemento de 2), a subtração se torna trivial. Para calcular \(A - B\), o computador simplesmente calcula \(A + \text{Complemento}(B)\).
Subtração na base 10 usando complemento à base
Subtração \(913 - 734 = 179\)
O complemento à base 10 de 734 é \(1000 - 734 = 266\). Assim, a subtração \(913 - 734\) pode ser transformada em uma adição: \(913 + 266 = 1179\).
Observe que o resultado final tem um dígito a mais que os números originais (\(1\)). Esse dígito é o “vai-um” que estoura a capacidade da representação. Ele é ignorado para obter o resultado correto (\(179\)).
Subtração na base 2 usando complemento à base
Vamos calcular \(25 - 19\) em binário (usando 5 bits para simplificar). Daí: \(25 = 11001_2\) e \(19 = 10011_2\)
Queremos fazer: \(11001_2 - 10011_2\).
Passo 1: Achar o complemento de 2 de 19 (\(10011\)). Usando a regra prática: mantém o último 1, inverte o resto à esquerda \(\to\) 01101.
Passo 2: Somar 25 com (-19). Transformamos a subtração em adição: \(11001 + 01101\).
Representando verticalmente a operação, temos:
$$ \begin{array}{c c c c c c} {} & \scriptscriptstyle 1 & & & \scriptscriptstyle 1 & \\ {} & 1 & 1 & 0 & 0 & 1 \\ {} + & 0 & 1 & 1 & 0 & 1 \\ \hline 1) & 0 & 0 & 1 & 1 & 0 \end{array} $$O bit “1)” à esquerda é o carry out que sai do número de bits. Ele é descartado. Resultado final: \(00110_2\) (que é 6 em decimal). Correto!
Limites da Representação (Range) #
Antes de falarmos sobre erros de cálculo, precisamos entender os limites do campo de jogo. Como a quantidade de bits é finita, existe um valor máximo e um mínimo que podemos escrever. Se tentarmos passar desses limites, o número “não cabe”.
O intervalo de valores depende de dois fatores:
- Quantos bits (\(N\)) temos disponíveis (8, 16, 32, 64…).
- Se estamos usando números Sem Sinal (Unsigned) ou Com Sinal (Signed).
Números Sem Sinal (Unsigned) #
Aqui, todos os bits são usados para representar a magnitude (o valor) do número. Não existem números negativos.
- Mínimo: É sempre 0 (todos os bits zero).
- Máximo: Todos os bits são 1. A fórmula é \(2^N - 1\).
Exemplo com 8 bits:
- De \(0\) a \(2^8 - 1\)
- De 0 a 255.
2. Números Com Sinal (Complemento de 2) #
Aqui, dividimos as combinações possíveis pela metade: metade para os positivos (e o zero) e metade para os negativos. O bit mais à esquerda (MSB) define o sinal, restando \(N-1\) bits para o valor.
A fórmula muda ligeiramente:
- Mínimo (Negativo): \(-2^{N-1}\)
- Máximo (Positivo): \(2^{N-1} - 1\)
Por que o máximo é um a menos que o módulo do mínimo? Porque o zero ocupa um lugar no lado dos “positivos” (onde o bit de sinal é 0). Em 8 bits, temos 256 combinações:
- 128 combinações negativas (de -1 a -128).
- 128 combinações não-negativas (0 a +127).
Tabela de Referência #
Veja como o alcance muda drasticamente dependendo da interpretação:
| Bits (\(N\)) | Tipo | Valor Mínimo | Valor Máximo |
|---|---|---|---|
| 4 bits | Sem Sinal | 0 | 15 |
| 4 bits | Com Sinal | -8 | +7 |
| 8 bits | Sem Sinal | 0 | 255 |
| 8 bits | Com Sinal | -128 | +127 |
| 16 bits | Sem Sinal | 0 | 65.535 |
| 16 bits | Com Sinal | -32.768 | +32.767 |
Assim, se você tem um sistema de 8 bits com sinal e tenta somar \(100 + 30\), o resultado matemático (130) é maior que o limite permitido (+127). O computador não tem bits suficientes para representar esse valor, causando o que chamamos de Overflow. Vamos entender isso melhor mais adiante. Mas antes, um detalhe importante.
O Paradoxo do Mínimo Valor (Um “Gotcha” Perigoso) #
Se você observar a tabela acima com atenção, notará uma assimetria: em 8 bits, vamos de -128 até +127.
Isso cria um problema curioso: o número -128 não tem um oposto positivo. O valor +128 simplesmente não cabe em 8 bits com sinal.
O que acontece se tentarmos aplicar a regra do “Inverte e Soma 1” justamente no
-128 (10000000)? Vamos ver:
- Número original:
10000000(-128) - Inverte os bits:
01111111(+127) - Soma 1:
Resultado: 10000000. Voltamos para o -128!
Isso significa que, computacionalmente, o inverso de -128 é ele mesmo. Esse comportamento causa bugs silenciosos em programação.
Em linguagens como C, C++ ou Java, tentar calcular o valor absoluto abs() ou
negar -x do menor número inteiro possível pode retornar o próprio número
negativo ou causar comportamento indefinido, pois o resultado positivo causaria
um Overflow imediato.
Aqui falamos de 8 bits, mas o mesmo vale para 16, 32 ou 64 bits. Sempre haverá um número negativo sem oposto positivo.
Overflow (Estouro de Capacidade) #
Nos exemplos acima, descartamos o último “vai-um” e o resultado estava certo. Mas nem sempre isso acontece.
O Overflow ocorre quando o resultado de uma operação é grande demais (positiva ou negativamente) para caber no número de bits disponível.
Lembre-se: em um sistema de 8 bits com sinal, podemos representar números de -128 a +127. Se você tentar somar \(100 + 30\), o resultado (130) não cabe, e invadirá o bit de sinal, parecendo um número negativo.
A detecção de overflow segue duas regras. A regra “humana” (lógica) e a regra “técnica” (hardware).
- Regra Humana: Se somarmos dois números Positivos e o resultado for Negativo, houve Overflow. (O mesmo vale para Negativo + Negativo = Positivo).
- Regra Técnica: Ocorre Overflow se o “vai-um” que entra no bit de sinal for diferente do “vai-um” que sai dele.
- Há um vai-um propagado para o bit de sinal sem vai-um saindo deste.
- Há um vai-um propagado pelo bit de sinal sem este ter recebido vai-um.
Escrevendo de outra forma: em complemento a dois, o overflow ocorre se o carry-in (vai-um que entra no bit de sinal) for diferente do carry-out (vai-um que sai do bit de sinal).
Vamos ver isso acontecendo na prática com números de 8 bits:
Overflow - Positivo + Positivo vira Negativo
Vamos somar \(88 + 46\). O resultado matemático seria 134. Mas em 8 bits com sinal, o máximo é 127. Vai dar erro.
Repare no Bit de Sinal (o mais à esquerda, bit 7).
$$ \begin{array}{c c c c c c c c c} {}&\scriptscriptstyle 1 & \scriptscriptstyle 1 & \scriptscriptstyle 1 & \scriptscriptstyle 1 & & & & & \\ {} & 0 & 1 & 0 & 1 & 1 & 0 & 0 & 0 \\ {} + & 0 & 0 & 1 & 0 & 1 & 1 & 1 & 0 \\ \hline {} & \textcolor{red}{1} & 0 & 0 & 0 & 0 & 1 & 1 & 0 \end{array} $$Análise:
- Olhe o bit de sinal (primeira coluna). Entrou um “vai-um” (recebeu 1 das posições anteriores).
- Mas não saiu nenhum “vai-um” dele para fora da conta.
- Entrou e não saiu? Overflow!
O resultado binário 10000110 começa com 1, indicando um número negativo (-122). Isso é obviamente um erro, pois somamos dois positivos.
Agora vejamos o caso oposto, com dois números negativos estourando o limite inferior (-128).
Overflow - Negativo + Negativo vira Positivo
Vamos fazer \( -76 + (-68) \). O resultado matemático seria -144. O limite inferior é -128. Vai dar erro.
Representamos em complemento de 2 e somamos:
$$ \begin{array}{c c c c c c c c c} \scriptscriptstyle 1 & & \scriptscriptstyle 1 & \scriptscriptstyle 1 & \scriptscriptstyle 1 & \scriptscriptstyle 1 & & & \\ {} & 1 & 0 & 1 & 1 & 0 & 1 & 0 & 0 \\ {} + & 1 & 0 & 1 & 1 & 1 & 1 & 0 & 0 \\ \hline \textcolor{red}{1} & 0 & 1 & 1 & 1 & 0 & 0 & 0 & 0 \end{array} $$Análise:
- Olhe o bit de sinal. Ele somou \(1 + 1\), gerando 0 e propagando um “vai-um” para fora (o \(\textcolor{red}{1}\) vermelho).
- Mas ele não recebeu nenhum “vai-um” da coluna anterior (veja o espaço vazio acima da segunda coluna).
- Saiu e não entrou? Overflow!
O resultado 01110000 começa com 0, indicando um número positivo (+112). Erro detectado.
Dica Extra: Identificando Negativos em Hexadecimal #
Na prática, programadores raramente olham para sequências de zeros e uns. A representação mais comum em depuradores (debuggers) e editores de memória é o Hexadecimal.
Como saber se um número Hex é negativo (em complemento de 2) apenas batendo o olho?
A regra é universal, não importa se o número tem 8, 16, 32 ou 64 bits: basta
olhar para o primeiro dígito da esquerda. Se esse dígito representar um valor
binário que começa com 1 (ou seja, \(\ge 8\)), o número inteiro é negativo.
Os “Dígitos Negativos” em Hexadecimal são: 8, 9, A, B, C, D, E, F
Exemplos:
- 8 bits:
0xFF\(\to\) Começa com F. Negativo (-1). - 16 bits:
0x8000\(\to\) Começa com 8. Negativo (-32.768). - 32 bits:
0xC0000005\(\to\) Começa com C. Negativo. - Qualquer tamanho:
0x7FFF...\(\to\) Começa com 7. Positivo.
Se você estiver depurando um código e vir uma variável inteira (signed)
começando com F (como 0xFFFFFFFF em 32 bits), saiba imediatamente que ela
vale -1.
Pode parecer estranho que o “maior” número visualmente (tudo preenchido com F ou 1) represente o -1. Mas podemos provar isso aplicando a regra do “Inverte e Soma 1” que acabamos de aprender.
Se quisermos saber qual é o valor de 0xFFFFFFFF (32 bits), fazemos o caminho reverso:
- Converter para Binário: Uma sequência de trinta e dois 1s (
1111...1111). - Inverter os bits: Todos os 1s viram 0s (
0000...0000). - Somar 1: $$ 0 + 1 = 1 $$
Como o bit de sinal original era 1 (negativo) e a magnitude encontrada foi
1, o valor final é -1.
Portanto, não importa o tamanho da variável (8, 16, 32 ou 64 bits): se todos os
bits forem 1 (ou todos os hexadecimais forem F), o número sempre será -1
em Complemento de Dois.
Na Prática: Visualizando em Python #
Se você tentar verificar o que aprendemos aqui usando o console do Python, terá uma surpresa. O Python lida com inteiros de forma diferente de linguagens como C ou Java: ele tem precisão arbitrária. Isso significa que os números podem ter quantos bits forem necessários, sem um limite fixo (como 8 ou 32 bits).
Por causa disso, a função nativa bin() não mostra o Complemento de Dois por
padrão. Ela apenas coloca um sinal de menos na frente:
>>> x = -5
>>> bin(x)
'-0b101' # Isso não é Complemento de Dois! É apenas "5 negativo".Para ver como os bits realmente são armazenados (como 1111...1011), precisamos
forçar o Python a limitar o número a uma quantidade de bits, usando uma
Máscara de Bits (Bitmask) com o operador AND (&).
Se quisermos ver a representação em 8 bits, usamos a máscara 0xFF (que é 11111111):
>>> x = -5
>>> bin(x & 0xFF)
'0b11111011' # Agora sim! Veja os bits ligados indicando o negativo.Se quisermos ver em 32 bits, usamos 0xFFFFFFFF:
>>> bin(x & 0xFFFFFFFF)
'0b11111111111111111111111111111011'Isso acontece porque o operador & trata o número como uma sequência de bits
pura, revelando a estrutura de Complemento de Dois que está “por baixo do capô”.
Conclusão #
O sistema de Complemento de Dois é uma das soluções mais elegantes da ciência da computação. Graças a ele, processadores podem realizar somas e subtrações usando o mesmo circuito lógico (o somador), apenas invertendo bits quando necessário. Isso simplifica drasticamente o design dos chips e reduz custos.
Com isso, fechamos o ciclo dos números inteiros. Sabemos representá-los, convertê-los e operá-los, inclusive os negativos.
Mas o que acontece quando precisamos de precisão além do inteiro? Como o
computador lida com \(0.5\) ou \(3.14\)? E por que o operador & funciona
como mostrado acima?
Essas perguntas serão respondidas nos próximos artigos da série Do Zero ao Float. Fique ligado!