Ir para o conteúdo principal

Como o Computador Subtrai Somando: Complemento de Dois e Overflow

Autor
Francisco Bustamante
Químico, cientista de dados e programador 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, e terminamos com uma pergunta: será que o hardware faz o mesmo, pedindo emprestado a bits vizinhos? E se eu te dissesse que o computador, na verdade, não faz a subtração?

Como vimos ao fechar aquele artigo, o problema não é o preço de um circuito subtrator — ele custa praticamente o mesmo que um somador. O que não faz sentido é manter os dois lado a lado fazendo trabalho quase idêntico. Para que um único circuito dê conta do recado, 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 a próxima potência de \(b\) e o número — nessa ordem: a potência em cima, o número embaixo.

Complemento à base 10

O complemento à base 10 de um número \(N\) é dado por \(10^d - N\), onde \(d\) é a quantidade de dígitos do sistema. Vamos representar o equivalente a “-734” num sistema de 3 dígitos, o menor que comporta o 734:

$$ \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).

O tamanho do sistema é uma escolha, e ele precisa ser respeitado do começo ao fim: no display de 4 dígitos do início do artigo, o complemento de 734 seria \(10^4 - 734 = 9266\), e é o 9266 que se comportaria como \(-734\) por lá. O raciocínio é o mesmo; muda só quantas casas o sistema tem. 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.

Até aqui, tudo bem. A pergunta que fica é: o que fazer com os outros bits? Reservar o MSB para o sinal é só metade da decisão, e historicamente houve três respostas diferentes para a outra metade (signed number representations).

Esquema Como escreve \(-5\) em 8 bits Quantos zeros existem Um mesmo somador serve para somar e subtrair?
Sinal-magnitude 10000101 (bit de sinal + o 5 de sempre) dois: 00000000 e 10000000 não
Complemento de um 11111010 (inverte os bits) dois: 00000000 e 11111111 não, exige um passo extra na soma
Complemento de dois 11111011 (inverte e soma 1) um só sim

Os dois primeiros são bem mais intuitivos para um humano — sinal-magnitude é literalmente “escreva o sinal e depois o número de sempre” — e mesmo assim perderam a disputa. O motivo tem nome: zero duplo. Nos dois esquemas, \(+0\) e \(-0\) são padrões de bits diferentes que valem exatamente a mesma coisa. Isso desperdiça uma das combinações disponíveis e, pior, obriga o hardware a checar dois padrões toda vez que alguém escreve if (x == 0). Some a isso o circuito adicional que ambos exigem na soma, e a disputa se resolve sozinha.

O complemento de dois tem um zero só, dispensa qualquer ajuste na hora de somar e é o que praticamente todo processador usa hoje. Ele cobra um preço, que vamos reencontrar adiante ao falar dos limites da representação: sobra uma combinação do lado negativo — justamente aquela que o \(-0\) ocuparia nos outros esquemas.

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.

Essa segunda leitura, a do peso negativo, fica bem mais concreta desenhada. Na régua abaixo, os oito bits aparecem com o peso de cada posição em cima e a parcela correspondente embaixo — exatamente como você faria com um número sem sinal. A única diferença está na ponta esquerda: aquele peso vem com sinal trocado, e é só isso que separa \(251\) de \(-5\).

Régua dos oito bits de 11111011 lidos como inteiro com sinal. Acima de cada bit está o seu peso: o primeiro, destacado em vermelho, vale menos 2 elevado a 7, ou seja, menos 128; os demais valem 64, 32, 16, 8, 4, 2 e 1. Abaixo de cada bit, uma seta aponta para a parcela correspondente: menos 128, 64, 32, 16, 8, 0, 2 e 1. A soma das parcelas resulta em menos 5.
Os mesmos oito bits, com um único peso trocado de sinal: é o bit da ponta que vira o número inteiro.

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^d - N\), num sistema de \(d\) bits)?

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.

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

Há uma imagem que arruma tudo o que vimos até aqui num desenho só, e vale a pena guardá-la. Em vez de pensar os números como uma reta que vai de \(-8\) a \(+7\), pense neles como um relógio: as combinações de bits dispostas em círculo, 0000 no topo, somar 1 sendo um passo no sentido horário. Está usando 4 bits na figura porque 16 posições cabem na página; em 8 bits é o mesmo relógio, só com 256 marcas.

Relógio do complemento de dois em 4 bits. As dezesseis combinações estão dispostas em círculo, com o padrão 0000 no topo. Descendo pela direita, em azul, vêm 0001 a 0111, valendo de mais 1 a mais 7. Subindo pela esquerda, em vermelho, vêm 1000 a 1111, valendo de menos 8 a menos 1. Uma seta no centro indica que somar 1 é girar um passo no sentido horário. A passagem entre mais 7 e menos 8, na parte de baixo do círculo, está destacada em amarelo e identificada como a costura onde ocorre o overflow. Uma anotação aponta que 1111 e 0000 são vizinhos, de modo que somar 1 a menos 1 dá zero.
As 16 combinações de 4 bits em círculo: somar 1 é girar um passo no sentido horário, e a costura entre +7 e −8 é onde o overflow acontece.

O relógio responde de uma vez a quatro perguntas que este artigo trata em seções separadas:

  • Por que o complemento funciona. Andar 5 passos para trás a partir do zero chega no mesmo lugar que andar 11 para a frente, porque o círculo fecha. Por isso subtrair é somar o complemento.
  • Por que a faixa é assimétrica. Existe uma marca só para o zero, no topo. Como as 16 posições se dividem em duas metades iguais, sobra uma combinação do lado negativo — e é por isso que vamos de \(-8\) a \(+7\).
  • O que é overflow. É atravessar a costura sem querer: somar dois positivos e cair do lado vermelho do círculo.
  • Por que \(-8\) não tem oposto. O \(+8\) simplesmente não existe no relógio. A posição onde ele cairia é ocupada pelo próprio \(-8\).

Vamos voltar a cada um desses pontos adiante, mas guarde o desenho.

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.

Essa assimetria não é acidente nem descuido de projeto: é o troco do zero único. Como o complemento de dois não gasta um padrão de bits com o \(-0\), sobra uma combinação do lado negativo — e essa combinação é o \(-128\). Nos esquemas que perderam a disputa lá atrás, a faixa era simétrica, mas ao custo de ter dois zeros.

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 C e C++, calcular o valor absoluto abs() ou negar -x do menor inteiro possível é comportamento indefinido: o padrão da linguagem não diz o que acontece, e o compilador pode otimizar o código assumindo que essa situação nunca ocorre.

Java não tem comportamento indefinido, mas isso não o salva do problema: Math.abs(Integer.MIN_VALUE) é especificado e devolve o próprio Integer.MIN_VALUE — um valor negativo saindo de uma função de valor absoluto. O bug é o mesmo nos dois casos; a diferença é que num deles ele é imprevisível e no outro está documentado.

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} $$

Aqui o \(\textcolor{red}{1}\) destacado é o bit de sinal do resultado: ele está dentro dos 8 bits, na coluna do bit 7.

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} $$

Cuidado: o \(\textcolor{red}{1}\) destacado neste exemplo não é o bit de sinal. É o vai-um que saiu da conta, numa coluna extra à esquerda dos 8 bits — e que será descartado. O bit de sinal do resultado é o 0 logo à direita dele.

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.

Quando o overflow sai do papel
#

Overflow não é curiosidade de sala de aula: está por trás de algumas das falhas mais caras já registradas em software.

Ariane 5, voo 501

Em 4 de junho de 1996, o foguete Ariane 5 explodiu 39 segundos após a decolagem, em seu voo inaugural. A causa: o sistema de referência inercial convertia a velocidade horizontal de um número de ponto flutuante de 64 bits para um inteiro com sinal de 16 bits. No Ariane 4, aquele valor sempre coubera. O Ariane 5 era mais rápido, o número não coube, e a conversão estourou.

O software tratou o erro parando o computador de bordo — e o computador reserva, que rodava exatamente o mesmo código, havia parado pelo mesmo motivo alguns milissegundos antes. Sem referência de atitude, o foguete girou, se partiu sob a pressão aerodinâmica e acionou a autodestruição. O prejuízo é estimado em US$ 370 milhões, causado por um trecho de código que nem precisava estar ativo naquele momento do voo.

Nem todo caso é trágico. Dois exemplos conhecidos do mesmo fenômeno:

  • Pac-Man, fase 256. O contador de fases tinha 8 bits, ou seja, ia de 0 a 255. Ao chegar na fase 256, o cálculo que desenha a tela estourou o contador e metade do labirinto virou lixo gráfico. Ficou conhecida como kill screen: a fase é intransponível, e por décadas foi o teto absoluto do jogo.
  • O contador do YouTube, em 2014. O número de visualizações era guardado num inteiro com sinal de 32 bits, cujo limite é \(2^{31} - 1 = 2.147.483.647\). Quando o vídeo de Gangnam Style passou dessa marca, o Google migrou o campo para 64 bits — e fez piada oficial sobre o assunto. Com 64 bits, o novo teto passa de 9 quintilhões.

Nos três casos o mecanismo é o mesmo que você acabou de ver nas contas de 8 bits acima. Muda a quantidade de bits e, com ela, o tamanho do estrago.

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, saiba imediatamente que ela é negativa. Só isso: o primeiro dígito conta o sinal, não o valor. 0xF0000000 também começa com F e vale \(-268.435.456\), o que é bem diferente de \(-1\).

Existe, porém, um caso particular inconfundível: quando todos os dígitos são F, como em 0xFFFFFFFF (32 bits), aí sim o valor é -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

print(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

print(bin(x & 0xFF))   # 0b11111011
# Agora sim! Veja os bits ligados indicando o negativo.

Se quisermos ver em 32 bits, usamos 0xFFFFFFFF:

x = -5

print(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ô”.

Tabela-Resumo
#

Conceito Regra Exemplo do artigo
Complemento à base \(b\) \(b^d - N\), num sistema de \(d\) dígitos \(10^3 - 734 = 266\)
Complemento de dois \(2^d - N\), num sistema de \(d\) bits \(2^4 - 5 = 1011_2\)
Atalho aritmético inverta todos os bits e some 1 0101 \(\to\) 1010 \(\to\) 1011
Atalho visual copie da direita até o primeiro 1, inverta o resto 011100 \(\to\) 100100
Ler um negativo subtraia \(2^d\) do valor sem sinal \(251 - 256 = -5\)
Ler um negativo (2ª forma) dê peso \(-2^{d-1}\) ao MSB \(-128 + 64 + 32 + 16 + 8 + 2 + 1 = -5\)
Subtrair somando \(A - B = A + \text{comp}_2(B)\), descartando o vai-um final \(25 - 19 = 6\)
Faixa sem sinal \(0\) a \(2^N - 1\) 8 bits: 0 a 255
Faixa com sinal \(-2^{N-1}\) a \(2^{N-1} - 1\) 8 bits: \(-128\) a \(+127\)
Por que a faixa é assimétrica existe um zero só, então sobra uma combinação negativa \(-128\) não tem oposto
Overflow (regra humana) positivo + positivo dando negativo, ou o contrário \(88 + 46 = -122\)
Overflow (regra técnica) o vai-um que entra no bit de sinal difere do que sai \(-76 + (-68) = +112\)
Negativo em hexadecimal primeiro dígito \(\ge 8\) 0xF0000000 é negativo
O caso do \(-1\) todos os dígitos iguais a F 0xFFFFFFFF vale \(-1\)
Em Python inteiros têm precisão arbitrária; use máscara para ver os bits bin(-5 & 0xFF) \(\to\) '0b11111011'

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