Ir para o conteúdo principal

Representação de Dados: Bits que Significam Algo

Autor
Francisco Bustamante
Químico, cientista de dados e programador Python.
Tabela de conteúdos
Por Dentro do Computador - Este artigo faz parte de uma série de artigos.
Parte 9: Esse Artigo

No artigo anterior — Pipeline e Barramentos — vimos como a CPU executa múltiplas instruções em paralelo e como os barramentos transportam informação entre os componentes. Mas o que é essa informação? Para o hardware, tudo são bits — tensões altas e baixas, zeros e uns. A diferença entre um caractere, um inteiro negativo e um número de ponto flutuante está exclusivamente na convenção de interpretação — na forma como acordamos que um padrão de bits deve ser lido.

Essa escolha não afeta apenas o software; ela molda o design físico do chip. A quantidade de bits que escolhemos para representar um dado dita diretamente a largura do barramento de dados, a capacidade dos registradores e a complexidade da UAL. A primeira dessas convenções que precisamos entender não lida com matemática pesada, mas com a forma como o computador aprendeu a ler e escrever conosco: o texto.

Este artigo percorre essas convenções fundamentais: os padrões para representar texto (ASCII, Unicode, UTF-8), os três métodos para inteiros com sinal (e por que o complemento a dois dominou), os deslocamentos de bits e o padrão IEEE 754 para números reais. Entender essas representações é essencial para compreender por que 0.1 + 0.2 ≠ 0.3 em programação e como números negativos funcionam em hardware.

Se você já leu “Do Zero ao Float”, pode passar por cima

Boa parte deste artigo retoma o que a série Do Zero ao Float cobre com muito mais profundidade: bases numéricas, complemento a dois, overflow, deslocamentos e IEEE 754. A repetição é deliberada — esta série precisa ser autocontida, e quem chegou aqui direto não deveria ter de sair para entender o resto. Mas se você vem de lá, a leitura útil é seletiva: as seções de caracteres e de representação de instruções na memória são novas, e o resto funciona como revisão rápida.

Este artigo, então, serve como um resumo integrado desses conceitos, com foco na perspectiva da arquitetura de computadores.

Representação de caracteres
#

O primeiro problema resolvido pelos computadores não foi o cálculo — foi a comunicação: como representar letras, dígitos e símbolos como bits? Para isso, o computador usa uma codificação de caracteres (character encoding): uma convenção que associa símbolos a números.

É importante separar três ideias que às vezes aparecem misturadas:

  • código de caracteres: a tabela de símbolos;
  • ponto de código: o número abstrato associado a um caractere;
  • codificação: a forma concreta de armazenar esse ponto de código em bytes.
Sistema Natureza Observação
ASCII Código de caracteres de 7 bits 128 símbolos; base histórica do texto em sistemas modernos
EBCDIC Código de caracteres de 8 bits Usado historicamente em ambientes IBM
Latin-1 (ISO 8859-1) Código de caracteres de 8 bits Extensão para línguas ocidentais
Unicode Repertório universal de pontos de código Define os caracteres; não define sozinho como armazená-los
UTF-8 / UTF-16 / UTF-32 Codificações Unicode Formas de armazenar pontos de código em bytes

ASCII, Latin-1 e o 8º bit
#

O ASCII (American Standard Code for Information Interchange) usa 7 bits e representa 128 símbolos. Ele inclui:

  • caracteres de controle (0 a 31);
  • caracteres imprimíveis (32 a 127), como letras, dígitos e pontuação.

Historicamente, em muitos sistemas o 8º bit do byte podia ser usado como bit de paridade para detectar erros de transmissão. Em outros contextos, esse bit passou a ser aproveitado para extensões de 8 bits, como o Latin-1, permitindo representar caracteres acentuados e símbolos adicionais.

Vale ver a tabela funcionando em números concretos, porque é aqui que a codificação deixa de ser abstração:

Caractere Decimal Hexadecimal Binário
'0' 48 0x30 0011 0000
'9' 57 0x39 0011 1001
'A' 65 0x41 0100 0001
'Z' 90 0x5A 0101 1010
'a' 97 0x61 0110 0001
'z' 122 0x7A 0111 1010

Duas coisas saltam dessa tabela. A primeira é que os dígitos começam em 0x30, o que torna a conversão de caractere para número uma subtração: '7' - '0' = 7. A segunda é mais bonita, e é exatamente o tema deste artigo:

$$\text{'a'} - \text{'A'} = 97 - 65 = 32 = 2^5$$

Maiúscula e minúscula do mesmo caractere diferem por um único bit — o bit 5. 'A' é 0100 0001 e 'a' é 0110 0001; muda o bit de peso 32 e mais nada. É por isso que trocar a caixa de uma letra ASCII não precisa de tabela nenhuma, só de uma operação bit a bit: c ^ 0x20 alterna entre maiúscula e minúscula, c | 0x20 força minúscula e c & 0xDF força maiúscula. A tabela ASCII não foi ordenada por acaso; essa regularidade foi projetada.

Unicode e codificações UTF
#

O Unicode não deve ser entendido como uma codificação fixa de 16 bits, mas como um repertório universal de pontos de código. Seu espaço de códigos vai de U+0000 até U+10FFFF.

Para armazenar esses pontos de código em memória ou em arquivos, usamos codificações como:

Codificação Comprimento Característica principal
UTF-8 1 a 4 bytes Compatível com ASCII nos primeiros 128 códigos
UTF-16 2 ou 4 bytes Usa 1 unidade de 16 bits no BMP e pares substitutos fora dele
UTF-32 4 bytes Um ponto de código por unidade de 32 bits

Assim, o correto é pensar:

  • Unicode = repertório de caracteres;
  • UTF-8 / UTF-16 / UTF-32 = maneiras de codificar esse repertório.
UTF-8 domina a prática moderna

O UTF-8 se tornou a codificação dominante porque preserva total compatibilidade com ASCII nos primeiros 128 códigos e usa comprimento variável apenas quando necessário. Para arquivos de texto, protocolos e páginas web, ele é hoje a escolha padrão.

Como o UTF-8 organiza os bits
#

Dizer que o UTF-8 usa “1 a 4 bytes” deixa de fora a parte interessante: como o programa sabe quantos bytes ler. O esquema é uma convenção de bits iniciais, e é a mesma ideia de campo de bits que o artigo inteiro vem tratando:

Faixa de pontos de código Bytes Padrão de bits
U+0000U+007F 1 0xxxxxxx
U+0080U+07FF 2 110xxxxx 10xxxxxx
U+0800U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
U+10000U+10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Os x são os bits do ponto de código, preenchidos da direita para a esquerda. O resto é sinalização: um byte que começa com 0 é ASCII puro; um que começa com 110 anuncia “sou o primeiro de dois”; e todo byte de continuação começa com 10.

Daí sai a propriedade que fez o UTF-8 ganhar: ele é auto-sincronizável. Como nenhum byte de continuação pode ser confundido com um byte inicial, um programa que cai no meio de um texto consegue achar o começo do próximo caractere andando para trás até encontrar um byte que não comece com 10. Nenhuma codificação de comprimento variável anterior tinha isso.

Um exemplo com o ç, ponto de código U+00E7:

  • U+00E7 em binário é 1110 0111 — 8 bits, então cai na faixa de 2 bytes;
  • os 11 bits do gabarito recebem 000 1110 0111;
  • primeiro byte: 110 + 00011 = 1100 0011 = 0xC3;
  • segundo byte: 10 + 100111 = 1010 0111 = 0xA7.

Ou seja, ç em UTF-8 são os bytes 0xC3 0xA7 — e é por isso que um arquivo UTF-8 lido como Latin-1 mostra ç no lugar do ç: os dois bytes aparecem como dois caracteres separados.

Para quem quiser a história completa dessa confusão, a referência canônica é The Absolute Minimum Every Software Developer Must Know About Unicode, de Joel Spolsky.

Inteiros sem sinal
#

Para inteiros sem sinal (unsigned), a representação é direta: cada bit corresponde a uma potência de 2, do LSB (bit menos significativo, \(b_0\)) ao MSB (bit mais significativo):

$$N_{10} = \sum_{i=0}^{n-1} b_i \cdot 2^i$$

Exemplo: \(10110101_2\):

$$= 1\cdot2^7 + 0\cdot2^6 + 1\cdot2^5 + 1\cdot2^4 + 0\cdot2^3 + 1\cdot2^2 + 0\cdot2^1 + 1\cdot2^0 = 128 + 32 + 16 + 4 + 1 = 181_{10}$$

Conversão binário ↔ hexadecimal
#

O hexadecimal funciona como uma notação compacta para binário: agrupe os bits de 4 em 4, da direita para a esquerda, e converta cada grupo para seu dígito hexadecimal equivalente (0–9, A–F).

Binário Grupos de 4 Hexadecimal Decimal
01011010 0101 + 1010 5A 90
11110011 1111 + 0011 F3 243

Verificação: \(\text{5A}_{16} = 5 \times 16 + 10 = 80 + 10 = 90_{10}\) ✓

Inteiros com sinal
#

Representar números negativos exige uma convenção adicional. Ao longo da história, três métodos foram usados:

Sinal e magnitude
#

O MSB indica o sinal (0 = positivo, 1 = negativo); os demais \(n-1\) bits representam o módulo. Intervalo: \(-(2^{n-1}-1)\) a \(+(2^{n-1}-1)\). O problema fatal: dois zeros — \(+0 = 0000\ 0000\) e \(-0 = 1000\ 0000\) — complicam circuitos de comparação e aritmética.

Complemento de 1 (C1)
#

O negativo de um número é obtido invertendo todos os bits. O resultado de \(-5\) em 8 bits: \(+5 = 0000\ 0101 \Rightarrow -5 = 1111\ 1010\). A aritmética fica mais simples do que no sinal e magnitude, mas o problema de dois zeros permanece (\(0000\ 0000\) e \(1111\ 1111\)). O C1 raramente é usado em sistemas modernos.

Complemento de 2 (C2) — o padrão moderno
#

O negativo é obtido invertendo todos os bits e somando 1 ao resultado. O C2 tem apenas um zero, e a subtração se reduz a uma adição — o hardware da UAL precisa de apenas um somador para realizar as duas operações.

$$\text{Intervalo:} \quad -2^{n-1} \leq x \leq +(2^{n-1}-1)$$
\(n\) bits Mínimo Máximo
8 \(-128\) \(+127\)
16 \(-32.768\) \(+32.767\)
32 \(-2.147.483.648\) \(+2.147.483.647\)

Com os três métodos definidos, vale vê-los lendo exatamente os mesmos bits. A figura abaixo faz isso com três bytes. O primeiro, positivo, dá o mesmo valor nos três esquemas. O segundo mostra o ponto central deste artigo: o mesmo byte vira três números diferentes, porque o valor não está nos bits, está na convenção usada para lê-los. E o terceiro, 1000 0000, é o caso-limite: um zero repetido no sinal e magnitude, o menor negativo do complemento de 1, e o \(-128\) que só o complemento de 2 consegue representar.

O mesmo byte lido em sinal e magnitude, complemento de 1 e complemento de 2
Três bytes, cada um lido pelos três esquemas de inteiros com sinal. Os positivos coincidem; os negativos divergem; e 1000 0000 vale −0, −127 ou −128 conforme a convenção.

Por que o complemento a 2 é o padrão?

Quatro razões concretas fazem do C2 o padrão universal:

  1. Subtração = adição: \(A - B = A + C2(B)\) — a UAL precisa apenas de um somador.
  2. Zero único: sem ambiguidade entre \(+0\) e \(-0\).
  3. Extensão de sinal trivial: para levar um número de 8 para 16 bits, basta repetir o bit de sinal à esquerda. \(11111011\) (\(-5\)) vira \(1111111111111011\), e continua valendo \(-5\). O complemento de 1 também tem essa propriedade; no sinal e magnitude, estender exige mover o bit de sinal de lugar.
  4. Comparação com os mesmos circuitos do inteiro sem sinal: somar, subtrair e comparar magnitudes usam exatamente o mesmo hardware que a versão unsigned — o que muda é apenas a interpretação das flags. Não há um segundo conjunto de circuitos para números com sinal.

O intervalo assimétrico (\(-128\) a \(+127\) em 8 bits) é às vezes listado como mais uma vantagem, mas não é: é consequência de ter só um zero. Com \(2^8\) padrões e um deles gasto no zero, sobram 255 para distribuir entre positivos e negativos, e a divisão não pode ser simétrica. E longe de ser vantagem, é a origem de um bug clássico: \(-128\) não tem oposto representável, então negar \(-128\) em 8 bits devolve \(-128\).

Operações em complemento a 2
#

Como calcular o C2 de um número (negação)
#

O algoritmo tem três passos:

  1. Escrever o número em binário
  2. Inverter todos os bits (obter o complemento de 1)
  3. Somar 1 ao resultado
Passo Bits
\(+5\) em binário 0000 0101
Inverter todos os bits (C1) 1111 1010
Somar 1 → resultado 1111 1011 = \(-5\) ✓

Verificação (\(+5 + (-5)\) deve ser zero):

$$0000\ 0101 + 1111\ 1011 = \underbrace{1}_{\text{carry}}\ 0000\ 0000 \xrightarrow{\text{descarta carry}} 0000\ 0000 = 0 \ \checkmark$$

Subtração como adição: \(A - B = A + C2(B)\)
#

Para entender como isso funciona na prática, vamos simular a operação 5 - 3. Seguindo a fórmula acima, definimos \(A = 5\) e \(B = 3\). Em vez de usar um circuito dedicado para subtrair, a CPU calcula o complemento de dois de 3 (obtendo a representação em bits de -3) e soma esse valor diretamente ao número 5. A tabela a seguir detalha esse processo passo a passo usando 8 bits:

Passo Operação Resultado
Representar \(+3\) binário 0000 0011
Inverter bits (C1) 1111 1100
Somar 1 → \(C2(3) = -3\) 1111 1101
Somar \(5 + (-3)\) 0000 0101 + 1111 1101 \(\underbrace{1}_{\text{carry}}\ 0000\ 0010\)
Descartar carry 0000 0010 = \(2\) ✓
Exemplo — Adição, subtração e overflow em 8 bits

Considere \(A = 00110010_2 = +50_{10}\) e \(B = 11010000_2\). Para determinar o valor de B: como o MSB é 1, é negativo. \(C2(B) = 00110000_2 = 48\), portanto \(B = -48_{10}\).

a) \(A + B\): \(00110010 + 11010000 = 1\ 00000010_2\) → descarta carry → \(00000010_2 = +2_{10}\) ✓ (\(50 + (-48) = +2\) — sinais opostos nunca geram overflow)

b) \(B - A = B + C2(A)\): \(C2(A)\): inverter \(00110010 = 11001101\) + 1 = \(11001110_2\) \(11010000 + 11001110 = 1\ 10011110_2\) → descarta carry → \(10011110_2\) MSB=1 → negativo: \(C2(10011110) = 01100010 = 98\) → resultado = \(-98_{10}\) (\(-48 - 50 = -98\) ✓ — dentro do intervalo \([-128, +127]\))

c) Dois positivos podem somar para um resultado negativo? Sim — isso é exatamente o overflow. Exemplo: \(C = 01000001_2 = +65\) e \(D = 01100000_2 = +96\): \(C + D = 10100001_2\) → MSB=1 → interpretado como negativo (\(-95\)), mas \(65 + 96 = 161 > 127\) → overflow

Detectando overflow
#

Overflow em complemento a 2

O overflow ocorre quando o resultado correto não cabe no número de bits disponíveis:

  • Dois positivos somados → resultado negativo = overflow ❌
  • Dois negativos somados → resultado positivo = overflow ❌
  • Positivo + Negativonunca gera overflow ✓

Regra de hardware: overflow ocorre quando o carry de entrada no bit de sinal é diferente do carry de saída do bit de sinal.

Caso essa parte do overflow tenha sido difícil para você, temos todo um artigo dedicado a isso na série Do Zero ao Float, onde explicamos o conceito de overflow em detalhes, com exemplos e exercícios.

Deslocamentos de bits (shifts)
#

Essa eficiência estrutural do Complemento a 2 não se limita apenas à facilidade de somar números negativos; ela permite que o hardware utilize “atalhos” matemáticos. A operação mais eficiente possível para multiplicar ou dividir por potências de 2 não aciona somadores complexos, mas sim os deslocamentos de bits (shifts) — uma operação resolvida “empurrando” bits para os lados em um único ciclo de UAL.

Operação Efeito matemático Bit inserido
Deslocamento à esquerda (1 bit) Multiplica por 2 0 à direita
Deslocamento lógico à direita (1 bit) Divide por 2 (inteiro sem sinal) 0 à esquerda
Deslocamento aritmético à direita (1 bit) Divide por 2 (inteiro com sinal) Réplica do MSB (bit de sinal)

A réplica do MSB funciona da seguinte forma: se o número é positivo (MSB = 0), o deslocamento à direita preenche com 0, mantendo o resultado positivo. Se o número é negativo (MSB = 1), o deslocamento preenche com 1, preservando o sinal negativo. Isso faz com que o deslocamento aritmético à direita se comporte como uma divisão inteira por 2, arredondando para baixo, mesmo para números negativos. Por exemplo: \(-3\) em 8 bits é 1111 1101. Deslocar à direita aritmeticamente resulta em 1111 1110, pois o MSB é 1, indicando um número negativo, e 1 é inserido à esquerda para manter o sinal. O resultado é \(-2\), que é o resultado esperado de \(-3\) dividido por 2, arredondando para baixo.

Exemplos em 8 bits:

Operação Antes Depois Valor
Shift esquerdo 0000 0011 (3) 0000 0110 6 = 3 × 2 ✓
Shift lógico direito 0000 0110 (6) 0000 0011 3 = 6 ÷ 2 ✓
Shift aritmético direito 1111 0000 (−16) 1111 1000 −8 = −16 ÷ 2 ✓

Deslocamento aritmético à direita: cuidado com a interpretação
#

O deslocamento aritmético à direita é frequentemente apresentado como uma forma rápida de “dividir por 2” inteiros com sinal. Isso é útil como intuição, mas merece uma observação:

  • para inteiros sem sinal, o deslocamento lógico à direita equivale à divisão inteira por 2;
  • para inteiros com sinal em complemento a 2, o deslocamento aritmético à direita preserva o bit de sinal, o que faz o resultado se comportar como um arredondamento para baixo em muitos casos negativos.

Vale então enunciar com cuidado: o deslocamento aritmético à direita é uma operação de hardware que, em complemento a 2, costuma corresponder à divisão inteira por potência de 2, preservando o sinal. “Costuma” porque o arredondamento não é o mesmo da divisão da linguagem: o deslocamento arredonda para baixo (na direção de \(-\infty\)), enquanto o operador de divisão inteira de muitas linguagens arredonda na direção do zero. Para \(-13\) em 8 bits, o deslocamento de 2 bits dá \(-4\); em C, -13 / 4 dá \(-3\).

Dica: aritmético × lógico

Use shift aritmético à direita para dividir inteiros com sinal (replica o bit de sinal, preservando-o). Use shift lógico para inteiros sem sinal (preenche com 0). Deslocar à esquerda multiplica por 2 em ambos os casos — basta garantir que não há overflow.

Exemplo — Divisão por 2 repetida com deslocamento aritmético

Considere \(A = 01011110_2 = +94_{10}\) e \(B = 10011101_2 = -99_{10}\).

Dividindo A (positivo) por 2 com shift aritmético à direita: \(01011110 \to 00101111 = 47 \to 00010111 = 23 \to 00001011 = 11 \to 00000101 = 5 \to 00000010 = 2 \to 00000001 = 1 \to 00000000 = 0\) Cada step divide por 2 (truncando), até atingir zero.

Dividindo B (negativo) por 2: o shift aritmético replica o bit de sinal (MSB=1), preservando o sinal negativo: \(10011101 \to 11001110 = -50 \to 11100111 = -25 \to 11110011 = -13 \to 11111001 = -7 \to 11111100 = -4 \to 11111110 = -2 \to 11111111 = -1 \to 11111111 = -1\) Para números negativos, o processo converge para −1 (não para zero), pois o bit de sinal nunca é eliminado.

Ponto flutuante: o padrão IEEE 754
#

Embora os inteiros em Complemento a 2 sejam perfeitos para contagens exatas e manipulação de memória, o mundo físico — com suas distâncias microscópicas e grandezas astronômicas — exige uma flexibilidade que os bits fixos não conseguem oferecer. Para grandezas contínuas, precisamos de números reais aproximados. O ponto flutuante resolve isso utilizando uma notação científica normalizada em base 2:

$$(-1)^S \times 1{,}\text{mantissa} \times 2^E$$

onde \(S\) é o bit de sinal (0 = positivo, 1 = negativo), \(E\) é o expoente inteiro e a mantissa são os bits fracionários após o 1. implícito.

Mas de onde vêm as regras para guardar isso em bits? Historicamente, cada fabricante de computador criava seu próprio jeito de representar números de ponto flutuante, o que causava um caos: um mesmo cálculo científico dava resultados diferentes dependendo de qual marca de computador o executasse. Para unificar a indústria, o IEEE (Institute of Electrical and Electronics Engineers — a principal organização mundial de padronização para engenharia elétrica e computação) publicou em 1985 a norma IEEE 754.

O padrão IEEE 754 original definiu com exatidão os formatos básicos de precisão simples e dupla, estabelecendo apenas requisitos mínimos para formatos “estendidos”. A tabela a seguir mostra os formatos de 32 e 64 bits definidos na norma, além do famoso formato extended de 80 bits (uma implementação específica da Intel/x86 que atende aos requisitos de formato estendido da norma):

Precisão Sinal Expoente Mantissa Total Bias
Single (32 bits) 1 bit 8 bits 23 bits 32 bits 127
Double (64 bits) 1 bit 11 bits 52 bits 64 bits 1.023
Extended (80 bits) 1 bit 15 bits 64 bits 80 bits 16.383

O expoente é armazenado com bias (viés): o valor real é o armazenado menos o bias, permitindo representar expoentes negativos sem bit de sinal próprio. O bit 1 à esquerda do ponto é implícito no single e double — não precisa ser armazenado, adicionando um bit extra de precisão efetiva (24 bits no single, 53 bits no double). Já no formato extended (80 bits), esse bit inteiro inicial é explícito e precisa ser armazenado, ocupando um espaço real e resultando em exatos 64 bits de precisão na mantissa. Hoje em dia, o formato extended de 80 bits é amplamente considerado legado: ele era o padrão interno dos antigos coprocessadores matemáticos (x87 FPU) da arquitetura x86. Processadores modernos geralmente realizam cálculos de ponto flutuante usando registradores vetoriais (SSE/AVX) otimizados para os formatos single e double. Contudo, o extended ainda é suportado nos processadores atuais por questões de retrocompatibilidade (sendo frequentemente mapeado para o tipo long double em compiladores C/C++ na arquitetura x86).

Abaixo, a estrutura de bits para o formato de precisão simples (32 bits) do IEEE 754, onde o bit de sinal é o mais significativo (MSB), seguido pelo expoente com bias e pela mantissa fracionária:

 31  30        23 22                       0
 ┌───┬──────────┬──────────────────────────┐
 │ S │ Expoente │        Mantissa          │
 │ 1 │  8 bits  │         23 bits          │
 └───┴──────────┴──────────────────────────┘

Casos especiais
#

O IEEE 754 reserva padrões específicos para situações limite:

Expoente Mantissa Significado
00000000 000...0 Zero (\(\pm 0\))
00000000 \(\neq 0\) Número denormalizado (subnormal)
11111111 000...0 \(\pm\infty\)
11111111 \(\neq 0\) NaN (Not a Number)
O vácuo perto do zero e o Underflow

Se você observar a matemática do IEEE 754, perceberá que existe um “salto” de precisão considerável entre o menor número normalizado possível e o zero absoluto. Para evitar que cálculos percam a precisão abruptamente e desabem para zero ao lidar com valores atômicos (um problema conhecido como underflow), o padrão introduziu os números denormalizados. Eles abrem mão do 1. implícito na mantissa para conseguir representar valores absurdamente pequenos (como \(1,4 \times 10^{-45}\) na precisão simples), preenchendo esse “vácuo” com uma degradação suave de precisão.

Convertendo para IEEE 754 single precision
#

flowchart TD
    A["Número decimal com sinal"] --> B["1. Converter para binário"]
    B --> C["2. Normalizar: 1.xxx × 2^E"]
    C --> D["3. Bit de sinal S
0 = positivo, 1 = negativo"] C --> Exp["4. Expoente com bias
E_real + 127"] C --> Mant["5. Mantissa
bits após o 1 implícito
(completar com zeros à direita)"] D --> R["Resultado: S | Expoente | Mantissa"] Exp --> R Mant --> R

Aplicando ao exemplo \(-5{,}75\):

Passo Operação Resultado
1. Converter para binário \(5 = 101_2\); \(0{,}75 \times 2 = 1{,}50\) → 1; \(0{,}5 \times 2 = 1{,}0\) → 1 \(-101{,}11_2\)
2. Normalizar Mover ponto 2 posições à esquerda \(-1{,}0111 \times 2^2\)
3. Bit de sinal Negativo 1
4. Expoente com bias \(E_{\text{real}} = 2\); \(2 + 127 = 129\) \(10000001_2\)
5. Mantissa Bits após 1.: 0111, completar com zeros 01110000000000000000000
Resultado final 1 10000001 01110000000000000000000

Verificação:

$$(-1)^1 \times 1{,}0111_2 \times 2^{129-127} = -1{,}0111_2 \times 2^2 = -101{,}11_2 = -5{,}75_{10} \ \checkmark$$
Exemplo — Decodificando um formato personalizado de 16 bits

Um formato de 16 bits usa: 1 bit de sinal, 5 bits de expoente com bias 15, e 10 bits de mantissa (normalizado — parte inteira implícita = 1).

Esse formato não é hipotético: é o binary16, ou half precision, do próprio IEEE 754 — e é o que as GPUs usam para treinar e executar modelos de aprendizado de máquina, onde metade da precisão em troca de metade da memória e do dobro da banda é um bom negócio.

Dado o padrão BA32₁₆ = 1011 1010 0011 0010₂:

  • Sinal: 1 → negativo
  • Expoente: 01110₂ = 14 → \(E_{\text{real}} = 14 - 15 = -1\)
  • Mantissa: 1000110010₂

Valor: \(-(1{,}1000110010_2) \times 2^{-1}\)

Convertendo a mantissa: \(1 + 2^{-1} + 2^{-5} + 2^{-6} + 2^{-9} = 1 + 0{,}5 + 0{,}03125 + 0{,}015625 + 0{,}001953\ldots \approx 1{,}5488\)

Resultado: \(-1{,}5488 \times 0{,}5 \approx -0{,}7744\)

Maior valor positivo normalizado: expoente = 11110₂ = 30 → \(E = 15\); mantissa = 1111111111₂ → \(1{,}1111111111_2 \times 2^{15} = 65504\)

Menor valor positivo normalizado: expoente = 00001₂ = 1 → \(E = -14\); mantissa = 0000000000₂ → \(1{,}0 \times 2^{-14} \approx 0{,}000061\)

Exemplo — Decodificando um número IEEE 754

Número com: sinal = 0 (positivo), mantissa = 1100100₂, expoente real = −3.

Número normalizado: \(+(1{,}1100100)_2 \times 2^{-3}\)

Convertendo a mantissa: \(1 + 2^{-1} + 2^{-2} + 2^{-5} = 1 + 0{,}5 + 0{,}25 + 0{,}03125 = 1{,}78125\)

Resultado: \(1{,}78125 \times 2^{-3} = 1{,}78125 / 8 = \mathbf{0{,}222656}\)

Aprofundamento: a série “Do Zero ao Float”

O padrão IEEE 754 tem muitas nuances além do que cobrimos aqui: arredondamento, números subnormais, operações especiais e as armadilhas de comparar floats. Se você quiser explorar esses tópicos em profundidade — incluindo por que 0.1 + 0.2 ≠ 0.3 em praticamente toda linguagem de programação — a série Do Zero ao Float cobre tudo isso com diversos exemplos.

Bônus Python: inspecionando representações de bits
#

Python oferece ferramentas diretas para inspecionar como números são armazenados em bits, usando o módulo struct (para ponto flutuante) e os operadores << e >> (para inteiros).

import struct

# Representação IEEE 754 single precision de -5.75
raw = struct.pack(">f", -5.75)          # ">f" = big-endian single float
print(f"-5.75 como bytes : {raw.hex()}")
# c0b80000

bits = int.from_bytes(raw, "big")
print(f"Em binário (32)  : {bits:032b}")
# 11000000101110000000000000000000
# S=1 | Exp=10000001 | Mant=01110000000000000000000 ✓

# Desempacotando de volta
valor = struct.unpack(">f", raw)[0]
print(f"De volta ao float: {valor}")
# -5.75 ✓

# Shifts e complemento a 2 com inteiros Python
a = 0b00110010  # +50 em decimal
b = 0b11010000  # em 8 bits sem sinal = 208, em C2 = -48

print(f"\na = {a}  ({a:08b})")
print(f"a << 1 = {a << 1}  ({a<<1:08b})  (× 2)")
print(f"a >> 1 = {a >> 1}  ({a>>1:08b})  (÷ 2)")

# Complemento a 2 em Python (Python usa inteiros de precisão arbitrária)
def c2_negacao(n: int, bits: int = 8) -> int:
    """Retorna -n em representação complemento a 2 com 'bits' bits."""
    mascara = (1 << bits) - 1          # 0xFF para 8 bits
    return (~n & mascara) + 1

print(f"\n-50 em C2 (8 bits) : {c2_negacao(50):08b} = {c2_negacao(50)}")
# 11001110 = 206 (em unsigned) ou -50 (em C2)

# Adição com truncamento para 8 bits
def soma_c2(a: int, b: int, bits: int = 8) -> int:
    mascara = (1 << bits) - 1
    return (a + b) & mascara

print(f"50 + (-48) em 8 bits: {soma_c2(a, b):08b} = {soma_c2(a, b)}")
# 00000010 = 2 ✓

# Verificando -5.75 como double (64 bits)
raw64 = struct.pack(">d", -5.75)
bits64 = int.from_bytes(raw64, "big")
print(f"\n-5.75 double (64 bits): {bits64:064b}")
# 1 10000000001 0111000000000000000000000000000000000000000000000000

Conclusão e próximos artigos
#

Bits sozinhos não têm significado — o significado emerge da convenção de interpretação. O ASCII transforma 7 bits em um caractere; o complemento a dois transforma um padrão de bits em um inteiro negativo; o IEEE 754 transforma 32 bits em um número fracionário de precisão. Entender essas convenções é entender como o computador funciona.

No próximo artigo, vamos explorar a representação de instruções de máquina — como os padrões de bits que compõem as instruções que a CPU executa, os diferentes formatos de instrução e os modos de endereçamento que determinam onde buscar os operandos. Compreender a representação de dados é o passo fundamental para entender a representação de instruções, pois as instruções também são apenas padrões de bits, mas com uma estrutura específica que o hardware interpreta para realizar operações.

Até lá!

Por Dentro do Computador - Este artigo faz parte de uma série de artigos.
Parte 9: Esse Artigo

Relacionados