Ir para o conteúdo principal

Como o Computador Subtrai Somando: Complemento de Dois e Overflow

Autor
Francisco Bustamante
Um químico trabalhando com Ciência de Dados e Programação em Python.
Tabela de conteúdos
Do Zero ao Float - Este artigo faz parte de uma série de artigos.
Parte 3: Esse Artigo

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”):

\[ 5 + 9999 = 10004 \]

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:

  • 00000101 representa +5.
  • 11111011 representa -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:

$$ 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 = 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:

  1. Col 0: \(0 < 1\). Somamos a base ao topo (\(0+2=2\)). \(2-1=\mathbf{1}\). Compensamos (+1) na próx.
  2. Col 1: Topo \(0\), Baixo \(0+1=1\). \(0 < 1\). Somamos base (\(2\)). \(2-1=\mathbf{1}\). Compensamos.
  3. Col 2: Topo \(0\), Baixo \(1+1=2\). \(0 < 2\). Somamos base (\(2\)). \(2-2=\mathbf{0}\). Compensamos.
  4. Col 3: Topo \(0\), Baixo \(0+1=1\). \(0 < 1\). Somamos base (\(2\)). \(2-1=\mathbf{1}\). Compensamos.
  5. 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:

  1. Pegue o número positivo: 0101
  2. Inverta todos os bits (0 vira 1, 1 vira 0): 1010
  3. 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.

$$ \begin{array}{r r r r r} & 1 & 0 & 1 & 0 \\ +{} & & & & 1 \\ \hline & 1 & 0 & 1 & 1 \end{array} $$

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:

  1. 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\).
  2. 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:

Regra prática para complemento a 2

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:

  1. Número: \(011\mathbf{1}00\)

    • Mantém o final 100.
    • Inverte o começo 011 \(\to\) 100.
    • Resultado: \(100\mathbf{1}00\)
  2. 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:

  1. Quantos bits (\(N\)) temos disponíveis (8, 16, 32, 64…).
  2. 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:

  1. Número original: 10000000 (-128)
  2. Inverte os bits: 01111111 (+127)
  3. Soma 1:
$$ \begin{array}{r r} & 01111111 \\ +{} & 1 \\ \hline & 10000000 \end{array} $$

Resultado: 10000000. Voltamos para o -128!

Isso significa que, computacionalmente, o inverso de -128 é ele mesmo. Esse comportamento causa bugs silenciosos em programação.

Cuidado 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).

  1. Regra Humana: Se somarmos dois números Positivos e o resultado for Negativo, houve Overflow. (O mesmo vale para Negativo + Negativo = Positivo).
  2. 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:

  1. Olhe o bit de sinal (primeira coluna). Entrou um “vai-um” (recebeu 1 das posições anteriores).
  2. Mas não saiu nenhum “vai-um” dele para fora da conta.
  3. 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:

  1. Olhe o bit de sinal. Ele somou \(1 + 1\), gerando 0 e propagando um “vai-um” para fora (o \(\textcolor{red}{1}\) vermelho).
  2. Mas ele não recebeu nenhum “vai-um” da coluna anterior (veja o espaço vazio acima da segunda coluna).
  3. 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:

  1. Converter para Binário: Uma sequência de trinta e dois 1s (1111...1111).
  2. Inverter os bits: Todos os 1s viram 0s (0000...0000).
  3. 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!

Do Zero ao Float - Este artigo faz parte de uma série de artigos.
Parte 3: Esse Artigo

Relacionados

Aritmética de Computadores: Soma e Subtração em Binário, Octal e Hexadecimal

Você não precisa reaprender matemática para calcular em binário. Descubra como a lógica da soma e subtração decimal se aplica a qualquer base. O artigo detalha os algoritmos formais de ‘vai-um’ e ‘compensação’, e ensina como implementá-los do zero em Python.