Hexadecimal: A Linguagem Secreta dos Programadores
O capítulo está quase se encerrando. Ao longo dele você aprendeu como computadores representam números, aprendeu como representam letras, aprendeu como representam idiomas inteiros. Você aprendeu algo que a maioria das pessoas sempre se perguntaram internamente e apenas aceitaram como se fosse mágica. Você sabe que por baixo de tudo, de cada letra, de cada emoji que você manda numa mensagem, de cada página de qualquer site que você visita, existem apenas zeros e uns. Bits agrupados em bytes, bytes formando números, números mapeados para símbolos por tabelas como o ASCII e o Unicode, respeitando padrões estabelecidos, como o UTF-8.
Até agora, você aprendeu como o computador representa a informação fisicamente, mas ainda não conversamos sobre o que é, de verdade, trabalhar com essa informação do outro lado - não usando o computador, mas construindo os softwares (códigos / programas) que rodam nele.
E é com essa perspectiva que você verá a necessidade de ainda uma outra forma de representar as coisas: o hexadecimal.
O que um programador realmente faz
Existe uma imagem popular do que é ser programador: uma pessoa digitando código numa tela escura, e as coisas simplesmente funcionando. E sabe o que é engraçado? Essa imagem não está errada! Não está errada, mas está longe de ser completa.
Uma parte enorme (eu diria que é a maior parte) do trabalho de um programador não é escrever código novo. É entender por que o código que já existe está se comportando da maneira que está se comportando. Irónico, não é? Um código não é feito por uma pessoa só e sim por várias. É preciso entender qual era a intenção do outro programador quando escreveu algum trecho de código que você está responsável por melhorar agora, ou até entender o seu próprio código de 6 meses atrás que você nem se lembra mais por que escreveu de tal jeito. Um cálculo que deveria dar 100 está dando 99. Um texto que deveria aparecer com acento está chegando corrompido. Um programa que deve terminar em segundos fica rodando por horas.
Esses problemas são inevitáveis. Todo programador os encontra, independentemente do nível de experiência ou do cuidado com que escreve o código. Encontrá-los e corrigi-los é uma habilidade tão fundamental quanto escrever o código em si. E esse processo tem um nome que você já ouviu pelo menos uma vez na vida: depuração, do inglês debugging.
Por que isso acontece?
Grande parte do tempo gasto tentando entender código não acontece apenas porque o código pode estar errado ou ter alguma falha em algum lugar, acontece porque, ao longo dos anos, muitos programadores se acostumaram a escrever código pensando apenas em fazê-lo funcionar e não necessariamente em escrevê-lo de forma fácil para ler e compreender depois.
Enquanto o código está fresco na cabeça de quem o escreveu, tudo parece claro. Mas dias, semanas ou meses depois, até o próprio autor pode ter dificuldade para entender o que estava pensando na epóca. E quando outra pessoa precisa trabalhar naquele código, a dificuldade pode ser ainda maior.
Isso é comum e acontece com todos. Grande parte das vezes você estará entusiasmado escrevendo código e não se preocupará com compreensibilidade logo de cara. Afinal, está compreensível para você. O erro é achar que também estará daqui a uns dias, ou que está para os outros.
No Ultimate Rust, você aprenderá não apenas a escrever código que funciona, mas também código claro, legível e compreensível, que outras pessoas - e o seu próprio "eu do futuro" - conseguem entender sem sofrimento.
A código com essas características, damos o nome de código idiomático. Nós voltaremos a esse assunto mais tarde.
Ao processo de ficar tentando entender porque o código está se comportando da maneira que está, dá-se o nome de depuração, do inglês debugging. A história por trás dessa palavra é curiosa.
Debugging: o que é um bug?
Em 1947, uma das primeiras programadoras da história, Grace Hopper, enquanto trabalhava na Universidade de Harvard a serviço da Marinha dos EUA, percebeu que o computador com que trabalhava estava com mal funcionamento, e descobriu a falha: um inseto real que havia entrado no hardware e interrompido o circuito. O inseto era uma mariposa. Ela colou a mariposa no diário de bordo com uma anotação "first actual case of a bug being found", que traduzido significa "primeiro caso real de um bug (inseto em inglês) sendo encontrado". O termo bug para problema num programa nasceu ali, e se popularizou de tal forma que debugging (remover o bug) virou sinônimo de investigar e corrigir falhas.

INFO
O diário de bordo com a mariposa colada existe até hoje e está exposto no Museu Nacional Americano de História. É um dos itens mais famosos da história da computação.
Depurar significa investigar por dentro
Depurar significa investigar. E investigar, por sua vez, em algum momento pode significar precisar olhar para dentro do computador. Eu não me refiro ao sentido literal de olhar para o computador físico - ou às vezes sim, como fez a nossa amiga Grace Hopper - e sim mais dentro ainda: dentro da memória. Se os programas do seu notebook, aplicativos do celular, etc... rodam na memória e você quer investigar o que está acontecendo passo a passo, você também vai querer saber quais valores estão sendo guardados na memória para ver se são os esperados, o que o programa está calculando com eles... Tudo isso para descobrir onde exatamente as coisas saíram do trilho e descobrir que parte do seu código causou o problema.
Mas veja bem o que isso significa. Você se lembra de como os dados estão guardados na memória? Exatamente! Em zeros e uns. Quando um programador precisa inspecionar esses dados para entender o que está acontecendo, ele está olhando para sequências de zeros e uns.
01000010 01000001 01011010 01001001 01001110 01000111 01000001 00100001Hoje existem ferramentas que tornam esse processo mais fácil e dificilmente você precisará olhar para zeros e uns literalmente. Elas convertem os valores para formas legíveis, que permitem pausar um programa e inspecionar cada variável. Mas essas ferramentas são uma conquista relativamente recente. Por décadas, não existiam e os programadores tinham que literalmente ficar vendo 0s e 1s aos montes.
Quando programadores liam folhas de papel
Nos anos 1950 e 1960, depurar um programa era um trabalho chato, manual e físico que hoje seria difícil de imaginar.
Praticamente não havia telas. Os computadores da época eram máquinas enormes que ocupavam salas inteiras, e "se comunicavam" com os programadores principalmente por meio de teletipos e impressoras, que imprimiam os resultados do programa em papel. Um exemplo perfeito para isso, é o próprio Harvard Mark II, o tal computador que a Grace Hopper e sua equipe utilizavam quando encontraram a mariposa. Como a maioria das máquinas da sua época, ele recebia programas por fitas perfuradas e produzia resultados impressos em papel, sem nenhuma tela.


Nota
Em minhas pesquisas, foi bastante difícil encontrar fotografias dessas máquinas com licença livre. Para o Harvard Mark II não encontrei nenhuma, por isso recorri ao seu predecessor (Mark I, 1944) e ao seu sucessor (Mark III, 1949). O Mark III também não ficou de fora dessa dificuldade: a única versão que encontrei com licença livre é esta, de qualidade bastante limitada.
Quando um programa dava errado nessas máquinas, a forma de investigar era solicitar ao computador algo chamado memory dump - em português, "despejo de memória". Não é um nome muito bonito, mas é bastante descritivo por si só: o computador simplesmente "despeja" tudo que estava na memória para uma folha de papel. Despejar aqui obviamente significa imprimir. Mas não se engane, não era um papel bonitinho com uma visualização organizada com variáveis e valores. Não era uma lista comentada explicando o que cada coisa significa e onde provavelmente estava o erro. Era o conteúdo bruto da memória, byte por byte, do jeito que o computador os armazena.
E como você já sabe muito bem, o computador armazena tudo de uma forma só.
Em zeros e uns.
Imagina acordar de manhã, tomar um café, chegar no trabalho, e receber isso para analisar:
01011001 01101111 01110101 00100000 01100001 01110010 01100101 00100000 01101100 01100101 01100001 01110010 01101110 01101001 01101110 01100111
00100000 01010010 01110101 01110011 01110100 00100000 01110111 01101001 01110100 01101000 00100000 01110100 01101000 01100101 00100000 01100010
01100101 01110011 01110100 00100000 01110010 01100101 01110011 01101111 01110101 01110010 01100011 01100101 00100000 01100101 01110110 01100101
01110010 00111010 00100000 01010101 01101100 01110100 01101001 01101101 01100001 01110100 01100101 00100000 01010010 01110101 01110011 01110100
00100000 00101000 01101000 01110100 01110100 01110000 01110011 00111010 00101111 00101111 01110101 01110010 01110101 01110011 01110100 00101110
01101111 01110010 01100111 00101001 00101110 00100000 01000010 01100101 01101100 01101111 01110111 00100000 01101001 01110011 00100000 01100001
01101110 00100000 01100101 01111000 01100001 01101101 01110000 01101100 01100101 00100000 01101111 01100110 00100000 01100001 00100000 01100011
01101111 01110010 01110010 01110101 01110000 01110100 01100101 01100100 00100000 01110100 01100101 01111000 01110100 00100000 01110100 01101000
01100001 01110100 00100000 01110000 01110010 01101111 01100111 01110010 01100001 01101101 01101101 01100101 01110010 01110011 00100000 01110011
01101111 01101101 01100101 01110100 01101001 01101101 01100101 01110011 00100000 01101000 01100001 01110110 01100101 00100000 01110100 01101111
00100000 01100100 01100101 01100010 01110101 01100111 00111010 00100000 11101111 10111111 10111101 11101111 10111111 10111101Esse não é um exemplo inventado para te assustar. Essa era a realidade cotidiana das pessoas que estavam construindo, do zero, o software que tornaria possível tudo que você usa hoje. Elas recebiam folhas assim (às vezes com dezenas de linhas, outras com centenas) e precisavam encontrar o erro escondido em algum lugar no meio disso tudo.
Tente ficar alguns segundos acompanhando os números sem se perder.
Difícil, não é? Agora imagina fazer isso como trabalho, várias vezes por dia.
Precisava de algo melhor. Mas o quê?
A solução óbvia que não funcionou: decimal
Você acabou de receber o memory dump impresso pelo computador. Seu programa falhou, você precisa entender o que aconteceu, e fica encarando aquele papel cheio de zeros e uns se perguntando por onde começar.
Se eu lhe conheço, meu caro leitor, a primeira coisa que provavelmente passou pela sua cabeça é:
Tudo bem, eu sei converter qualquer binário para decimal. Aprendi na seção 1.4. É trabalhoso, mas é possível. Vou converter tudo e aí consigo ler os valores.
É um pensamento completamente razoável, de fato você conseguiria. Vamos seguir ele até o fim e ver onde ele leva.
Fazendo a conta de verdade
Vamos pegar o primeiro byte daquele dump: 01011001. Você já sabe o processo: cada bit vale uma potência de 2 dependendo da sua posição:
Resultado: 89. Agora, cruzando com a tabela ASCII da seção 1.5, vemos que 89 equivale a Y. Funcionou! Agora é só repetir isso para os outros 173 bytes do dump.
Pare para pensar um pouco. Pense no que acabamos de fazer para converter um único byte.
Nós fizemos 8 multiplicações e 8 somas. Para um byte. 174 bytes ao todo, então multiplicamos isso por 174. Seriam 174 vezes 8 multiplicações e 8 somas, totalizando 1392 multiplicações + 1392 somas - feitas à mão, num papel, para converter apenas esse trecho pequeno de um dump, que na realidade seria muito maior. Na prática, dumps reais podiam ter centenas ou milhares de bytes.
Mas sabe o que é pior do que a quantidade absurda de contas?
O perigo silencioso
Imagina que você está quase na metade do processo, já converteu 80 bytes diferentes, está cansado, e no byte 81 você comete um pequeno erro: confunde 2⁴ com 2⁵ numa multiplicação. Você vai chegar obiviamente a um número diferente do valor real (e boa sorte para perceber se foi erro do computador ou se foi seu, e se foi seu, aonde exatamente você errou).
E agora?
Agora você não sabe. Não tem nenhum sinal de alerta. Nenhuma indicação que algo deu errado. Você vai olhar para aquele número incorreto e acreditar que aquele é o valor real que estava na memória. Vai tomar decisões de depuração desnecessárias baseadas num dado que você mesmo inventou sem querer.
Num processo de depuração, um erro silencioso não é só ineficiente, é perigoso. Pode te fazer tentar corrigir um código que estava certo, ou ignorar o código que realmente estava errado. Você pode passar horas ou dias numa direção completamente equivocada e acabar criando mais problemas ainda antes de perceber que o problema era outro.
E mesmo que você não cometesse nenhum erro, mesmo que sua atenção fosse perfeita durante todas as mais de 1000+ operações, o processo todo seria absurdamente lento para uso cotidiano.
E novamente: se eu consegui construir seu fluxo de pensamento do jeito que eu esperava, agora você deve estar se perguntando:
"Mas e se o computador convertesse para mim?"
Faz todo sentido perguntar isso. Se o problema é que a conversão manual é lenta e propensa a erros, por que não programar o próprio computador para converter o dump para decimal antes de imprimir?
A verdade é que isso foi tentado. E não funcionou tão bem quanto parecia.
O motivo é que converter binário para decimal não era uma operação simples nem mesmo para o computador. Ela exige uma série de divisões sucessivas, que eram das operações mais custosas que um computador dos anos 1960 podia executar. A velocidade com que esses computadores operavam eram magnitudes menor da velocidade dos computadores de hoje, e converter um dump inteiro em decimal levaria um tempo tão grande que não compensava. Como o objetivo do dump era ser rápido - algo como uma fotografia imediata do estado da memória no momento da falha - desperdiçar tempo com conversão antes de imprimir era problemático.
Mas tem ainda outro problema e ainda mais profundo no decimal, que vai além de velocidade. Um problema estrutural que não desaparecere mesmo que a conversão seja instantânea.
O problema que a velocidade não resolve
Vamos dar um passo para trás e pensar em algo que parece óbvio mas que tem uma consequência muito importante: por que nós queremos ler em decimal?
Por pura conveniência. Literalmente. Crescemos aprendendo decimal, pensamos em decimal, compramos pão em decimal. Decimal é a nossa língua nativa para números.
O computador, porém, nunca ouviu falar de decimal. Ele não tem motivo histórico. físico ou matemático para ter qualquer relação especial com o número 10. Ele trabalha com dois estados apenas, e o único sistema numérico que tem uma relação natural e direta com isso é o binário.
Isso tem uma consequência direta que você sente na hora quando tenta trabalhar com um dump em decimal, mesmo que o dump inteiro já esteja convertido. O grande problema é: decimal não tem relação natural nenhuma com grupos de bits.
Por exemplo, pega o byte 11111111. Quantas multiplicações você teria que fazer para saber que esse é o valor 255? 8. E se eu te perguntar o contrário: qual é o byte do decimal 200 em binário? Você precisa fazer várias divisões sucessivas por 2 e guardando os restos, até chegar lá. Não existe nenhum atalho, nenhum padrão que você memoriza e aplica sem fazer contas. Toda conversão exige uma conta do início ao fim.
Como seria essa conversão?
Na seção sobre binários, você apenas aprendeu a converter números binários para decimal, mas não o inverso. Isso não foi acidental. Escolhi não ensinar esse processo inverso por fins didáticos. Não iria acrescentar em nada ao objetivo de construção mental necessária para você entender cada seção, e você aprendeu o suficiente para entender cada conceito.
Você não precisará converter números decimais para binário no dia a dia (e provavelmente o inverso também não), mas, para caso você tenha curiosidade, abaixo deixo uma imagem que mostra como seria isso.
200 em binário é 11001000.
Mesmo que o computador tenha feito toda conversão para você, depurar não é só ler valores. É pensar sobre eles. Quando você olha para 200 num dump em decimal, você consegue ler só o número, mas o que ele significa em bytes? Esse valor está próximo do limite que um byte consegue guardar? Ele caberia num único byte ou precisaria de dois? Você não sabe. Não tem como saber só de olhar. Precisaria acabar calculando.
Com binário, você olha para 11001000 e vê diretamente o que está na memória. Todo o objetivo aqui sempre foi esse: ler o que está na memória. Cada dígito é um bit, sem nenhuma camada entre você e o dado.
O problema do binário não é que ele esconde alguma coisa. É só que ele é longo demais para ser legível.
O que se precisava era de um sistema que mantivesse essa transparência do binário, onde olhar para um símbolo significasse ver diretamente os bits, mas que fosse compacto o suficiente para um ser humano conseguir ler. Decimal não é isso. Precisava de outra coisa.
A segunda ideia: inventar símbolos novos
Tudo bem. Decimal não funciona. Vamos tentar outra abordagem.
Se o problema é que nenhum sistema existente serve, que tal criar um do zero? Um sistema feito especificamente para representar bytes, sem nenhum compromisso com o que já existe. Você define as regras, define os símbolos, define tudo.
E a primeira decisão seria: quantos símbolos criar?
Um byte pode ter 256 valores diferentes, de 0 a 255. E se você criasse exatamente 256 símbolos, um para cada valor possível? Qualquer byte poderia ser representado por um único caractere. Um símbolo, um byte.
Funcionaria! E parece a solução ideal. Mas antes de comemorar, vamos parar por um segundo e pensar no que seria trabalhar com esse sistema no dia a dia.
Você teria 256 símbolos para aprender. Não são 26 letras do alfabeto que você foi assimilando ao longo de anos, com contexto e repetição em todos os cenários. Não são 10 dígitos que você usa desde criança para contar troco e marcar pontos num jogo. São 256 desenhos novos, criados do nada, cada um representando um valor diferente, sem nenhuma lógica conectado um ao outro. Sem história, sem contexto, sem nenhum padrão que ajude a memória além de decorar que x símbolo significa o byte 10101010.
Isso já seria um problema sério, mas tem algo pior.
Com 256 símbolos inventados sem relação entre eles, você não conseguiria raciocinar. Se eu colocasse dois desses símbolos na sua frente, excepto se você tivesse decorado todos os 256, você não teria como saber qual dos dois representa um valor maior sem consultar uma tabela. Mentalmente você não conseguiria comparar, somar, perceber se um valor já está próximo do limite de um byte. Os símbolos não teriam estrutura interna nenhuma. Seriam apenas desenhos.
E aí você percebe que esse sistema seria ainda pior do que o binário. O binário pelo menos tem uma lógica clara: 0 é zero, 1 é um, e você consegue raciocinar com eles depois de algum estudo. Com 256 símbolos inventados, você teria memorizado uma tabela enorme e ainda assim não conseguiria pensar com ela.
A ideia não foi a frente. Ficou claro que o caminho não era inventar algo do zero.
O que precisava existir era um sistema que tivesse relação natural e direta com os bits e bytes. Não algo novo. Algo já familiar, só que escolhido com muito mais cuidado do que o decimal foi.
A pergunta correta não era "que símbolos novos podemos criar?". Era "em qual base numérica os bits fazem sentido?"
O que realmente precisava existir?
Decimal não funciona. Inventar símbolos novos não funciona. O que exatamente precisaria existir para resolver o problema?
Quando você para para pensar com cuidado nos motivos pelos quais essas tentativas falharam, três requisitos aparecem normalmente. Não são requisitos que alguém sentou e listou numa reunião. São requisitos que a própria existência foi revelando conforme as soluções menos eficientes foram sendo tentadas e descartadas.
- Precisava ser compacto. Esse era o problema original. Para representar um byte em binário precisamos de 8 caracteres, e isso é longo demais para um ser humano conseguir ler com rapidez e confiança. O novo sistema precisava representar um byte com bem menos símbolos do que isso.
- Precisava ter lógica interna. Os 256 símbolos inventados falharam exatamente por não ter isso. O novo sistema precisava ter uma estrutura matemática, como decimal tem, que permite aprender, comparar valores e raciocinar sem precisar consultar uma tabela a cada passo.
- Precisava ter uma relação direta com os bits. Esse é o requisito mais importante, e é o que eliminou o decimal. A conversão entre o novo sistema e o binário precisava ser imediata, sem cálculos, sem multiplicações, sem divisões. Você olha para um símbolo e sabe na hora quais bits ele representa. Você olha para um grupo de bits e sabe na hora qual símbolo ele é.
É esse terceiro requisito que aponta o caminho para a solução. Pense no que "relação direta com os bits" significa. Se um símbolo do novo sistema sempre representa exatamente o mesmo grupo de bits, sem exceção, a substituição se torna pura substituição. Como trocar uma palavra por outra numa tradução onde cada palavra tem sempre um único equivalente e a correspondência nunca muda. Nenhuma conta. Só substituição.
Mas para isso funcionar, o número de símbolos do sistema precisa ser exatamente igual ao número de combinações possíveis de algum grupo de bits. E você já sabe como calcular isso:
- Com 1 bit você tem 2 combinações.
- Com 2 bits você tem 4 combinações.
- Com 3 bits você tem 8.
- Com 4 você tem 16.
O número de combinações é sempre uma potência de 2.
Isso significava que o sistema precisava ter exatamente 2, 4, 8, 16, 32... símbolos (alguma dessas alternativas). Qualquer outro número e a correspondência direta quebraria porque sempre faltaria ou sobraria combinações de bits sem símbolos associadas a eles.
Quais são os candidatos?
Com essa restrição em mente, as opções ficam bem definidas. Veja o que cada uma significaria na prática para trabalhar com bytes:
| Símbolos diferentes (Base) | Cada símbolo representa | Para escrever 1 byte precisamos de |
|---|---|---|
| 2 Binário | 1 bit | 8 caracteres |
| 4 Quaternário | 2 bits | 4 caracteres |
| 8 Octal | 3 bits | ??? |
| 16 Hexadecimal | 4 bits | 2 caracteres |
| 32 Base-32 | 5 bits | ??? |
2 símbolos é o próprio binário. Já temos isso e também sabemos que 1 byte precisa de 8 bits. Não resolve nada.
4 símbolos: cada símbolo representaria um grupo de 2 bits dentro de um byte, logo, um byte precisaria de 4 caracteres. É mais compacto que o binário, mas ainda são 4 caracteres por byte. É uma melhora pequena, mas longe do ideal.
8 símbolos: cada símbolo representa 3 bits. Aqui está o problema do primeiro ???: 1 byte tem 8 bits, e 8 não é divisível por 3. Dois caracteres cobririam 6 bits, e faltariam outros 2 bits. Três caracteres cobririam 9 bits, sobraria 1. Não tem como ter um número inteiro de caracteres no sistema octal que represente exatamente um byte. A correspondência não fecha. Veremos mais adiante que esse sistema foi tentado mesmo assim, e os problemas que ele causou.
16 símbolos: cada símbolo representa um grupo de 4 bits. E o 8 dividido por 4 é exatamente 2, sem sobra nenhuma. Um byte vira sempre 2 símbolos. Sempre, sem exceção. A correspondência fecha perfeitamente!
32 símbolos: o mesmo problema do octal aparece de novo: cada símbolo representaria 5 bits, e 8 não é divisível por 5. E mesmo que fosse, 32 símbolos para aprender é muita coisa.
A opção de 16 símbolos é claramente o ponto de equilíbrio perfeito: compacto, com lógica interna, e com uma relação direta com os bytes. Mas antes de ir para ela, vale contar a história octal - porque esse sistema foi realmente usado por anos, e inclusive até hoje é usado em alguns cantos (como em algumas coisas no linux), e seja possível você acabar se deparando com ele. Além de que entender porque falhou ajuda a entender por que o hexadecimal sobreviveu.
A tentativa que quase funcionou: o octal
O sistema de 8 símbolos (também conhecido por base-8) tem um nome: octal. É um sistema de base 8, onde os símbolos são simplesmente os dígitos de 0 a 7.
E você deve estar se perguntando: se acabamos de ver que o octal não funciona com bytes, por que raios alguém o usou?
A resposta é que, por muito tempo, bytes não eram o padrão. Isso pode parecer estranho depois de tudo que você aprendeu neste capítulo, mas é verdade. Nos anos 1950 e boa parte dos 1960, os computadores não tinham uma unidade padrão de memória ainda. Diferentes máquinas trabalhavam com grupos de bits de tamanhos diferentes - algumas usavam 6 bits como unidade básica, outras usavam 12, outras usavam 36. Era uma época em que a indústria ainda estava descobrindo como construir computadores, e cada fabricante tomava suas próprias decisões. Esse é um exemplo perfeito de quando na subseção Como nasce um código, na seção 1.5, eu disse que essa época era "um caos" na computação.
Nesse cenário, o octal fazia todo sentido. O PDP-8 da DEC, por exemplo, lançado em 1965 e um dos computadores mais influentes da história, trabalhava com palavras de 12 bits, e 12 é divisível por 3. Cada símbolo octal representa 3 bits, a divisão fechava perfeitamente, e a conversão era tão mecânica quanto a gente precisa que seja. Manuais da época eram cheios de números octais. Programadores aprendiam a pensar em octal e funcionava bem.

Mas conforme o byte de 8 bits foi se tornando o padrão universal - como você já viu na seção 1.6, o problema matemático que já existia no octal ficou impossível de ignorar. 8 bits não se encaixava num sistema de divisão por 3, e nunca vai ser.
Isso significa que não existia nenhuma forma de fazer o octal funcionar limpo com bytes. Não era um problema que podia ser resolvido com uma convenção nova ou um padrão melhor. A incompatibilidade estava na raiz. Na relação entre o tamanho do símbolo e o tamanho do byte. E foi exatamente essa raiz que ajudou a entrar a solução certa: se o problema era que 3 bits por símbolo não encaixava em um byte, a pergunta que surgiu naturalmente foi quantos bits por símbolo encaixam. A resposta - mais conveniente, como vimos, era 4. E um sistema onde cada símbolo representa exatamente 4 bits tem exatamente 16 símbolos possíveis.
É aí que entra o hexadecimal.
A solução elegante: hexadecimal
Um sistema onde cada símbolo representa exatamente 4 bits. 4 bits por símbolo, 8 bits por byte, portanto 2 símbolos por byte. A conta fecha perfeitamente.
Esse sistema tem um nome: hexadecimal. Do grego hexa, que significa seis, e do latim decem, que significa dez. Dezesseis.
De onde vieram os 16 símbolos?
Agora surge a pergunta: quais são esses 16 símbolos?
O sistema decimal já nos dá 10 que todo mundo conhece e está acostumado: 0, 1, 2, 3, 4, 5, 6, 7, 8 e 9. Continuar usando esses seria uma ideia muito boa e tornaria mais fácil de aprender. Mas o hexadecimal precisa de 16. Faltam 6.
Aqui a decisão foi a mais simples possível. Lembra que descartamos a ideia de inventar símbolos novos porque ninguém os conheceria e não teriam lógica interna? A solução foi pegar os 6 símbolos restantes do alfabeto: A, B, C, D, E e F - símbolos que qualquer pessoa alfabetizada já conhecia e sabia escrever e saber de cabeça qual é maior (qual vem depois de qual), sem precisar aprender nada novo.
Se paramos no 9, a seguir vem o A, que representa dez. O B representa onze. o C representa doze. E assim por diante, até o F, que representa quinze. Não tem nenhum mistério nisso. É simplesmente a continuação dos digitos decimais onde eles acabam até o 15.
Uma dica importante sobre sistemas numéricos
No sistema decimal existem 10 símbolos possíveis: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.
A contagem começa em 0, e o 0 também conta como símbolo. Por isso, mesmo indo apenas até 9, o sistema possui dez símbolos diferentes. Entretanto, não é por possuir 10 símbolos que significa que o maior valor seja 10. Na verdade, 10 já é um número formado por dois símbolos: 1 e 0.
O sistema hexadecimal segue exatamente a mesma lógica. Ele possui 16 símbolos diferentes: 0-9 e A-F. E é importante não confudir achando que a contagem vai até 15. Ela vai até 15. A contagem também começa em 0 e vai até o último símbolo (F), que representa o valor 15, não 16.
Decimal:
0 1 2 3 4 5 6 7 8 9Hexadecimal:
0 1 2 3 4 5 6 7 8 9 A B C D E FO resultado é a tabela abaixo. Guarda ela com carinho, porque o hexadecimal estará presente ao longo de quase toda sua vida como programador:
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 0 | 00 | 0000 0000 |
| 1 | 01 | 0000 0001 |
| 2 | 02 | 0000 0010 |
| 3 | 03 | 0000 0011 |
| 4 | 04 | 0000 0100 |
| 5 | 05 | 0000 0101 |
| 6 | 06 | 0000 0110 |
| 7 | 07 | 0000 0111 |
| 8 | 08 | 0000 1000 |
| 9 | 09 | 0000 1001 |
| 10 | 0A | 0000 1010 |
| 11 | 0B | 0000 1011 |
| 12 | 0C | 0000 1100 |
| 13 | 0D | 0000 1101 |
| 14 | 0E | 0000 1110 |
| 15 | 0F | 0000 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 16 | 10 | 0001 0000 |
| 17 | 11 | 0001 0001 |
| 18 | 12 | 0001 0010 |
| 19 | 13 | 0001 0011 |
| 20 | 14 | 0001 0100 |
| 21 | 15 | 0001 0101 |
| 22 | 16 | 0001 0110 |
| 23 | 17 | 0001 0111 |
| 24 | 18 | 0001 1000 |
| 25 | 19 | 0001 1001 |
| 26 | 1A | 0001 1010 |
| 27 | 1B | 0001 1011 |
| 28 | 1C | 0001 1100 |
| 29 | 1D | 0001 1101 |
| 30 | 1E | 0001 1110 |
| 31 | 1F | 0001 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 32 | 20 | 0010 0000 |
| 33 | 21 | 0010 0001 |
| 34 | 22 | 0010 0010 |
| 35 | 23 | 0010 0011 |
| 36 | 24 | 0010 0100 |
| 37 | 25 | 0010 0101 |
| 38 | 26 | 0010 0110 |
| 39 | 27 | 0010 0111 |
| 40 | 28 | 0010 1000 |
| 41 | 29 | 0010 1001 |
| 42 | 2A | 0010 1010 |
| 43 | 2B | 0010 1011 |
| 44 | 2C | 0010 1100 |
| 45 | 2D | 0010 1101 |
| 46 | 2E | 0010 1110 |
| 47 | 2F | 0010 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 48 | 30 | 0011 0000 |
| 49 | 31 | 0011 0001 |
| 50 | 32 | 0011 0010 |
| 51 | 33 | 0011 0011 |
| 52 | 34 | 0011 0100 |
| 53 | 35 | 0011 0101 |
| 54 | 36 | 0011 0110 |
| 55 | 37 | 0011 0111 |
| 56 | 38 | 0011 1000 |
| 57 | 39 | 0011 1001 |
| 58 | 3A | 0011 1010 |
| 59 | 3B | 0011 1011 |
| 60 | 3C | 0011 1100 |
| 61 | 3D | 0011 1101 |
| 62 | 3E | 0011 1110 |
| 63 | 3F | 0011 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 64 | 40 | 0100 0000 |
| 65 | 41 | 0100 0001 |
| 66 | 42 | 0100 0010 |
| 67 | 43 | 0100 0011 |
| 68 | 44 | 0100 0100 |
| 69 | 45 | 0100 0101 |
| 70 | 46 | 0100 0110 |
| 71 | 47 | 0100 0111 |
| 72 | 48 | 0100 1000 |
| 73 | 49 | 0100 1001 |
| 74 | 4A | 0100 1010 |
| 75 | 4B | 0100 1011 |
| 76 | 4C | 0100 1100 |
| 77 | 4D | 0100 1101 |
| 78 | 4E | 0100 1110 |
| 79 | 4F | 0100 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 80 | 50 | 0101 0000 |
| 81 | 51 | 0101 0001 |
| 82 | 52 | 0101 0010 |
| 83 | 53 | 0101 0011 |
| 84 | 54 | 0101 0100 |
| 85 | 55 | 0101 0101 |
| 86 | 56 | 0101 0110 |
| 87 | 57 | 0101 0111 |
| 88 | 58 | 0101 1000 |
| 89 | 59 | 0101 1001 |
| 90 | 5A | 0101 1010 |
| 91 | 5B | 0101 1011 |
| 92 | 5C | 0101 1100 |
| 93 | 5D | 0101 1101 |
| 94 | 5E | 0101 1110 |
| 95 | 5F | 0101 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 96 | 60 | 0110 0000 |
| 97 | 61 | 0110 0001 |
| 98 | 62 | 0110 0010 |
| 99 | 63 | 0110 0011 |
| 100 | 64 | 0110 0100 |
| 101 | 65 | 0110 0101 |
| 102 | 66 | 0110 0110 |
| 103 | 67 | 0110 0111 |
| 104 | 68 | 0110 1000 |
| 105 | 69 | 0110 1001 |
| 106 | 6A | 0110 1010 |
| 107 | 6B | 0110 1011 |
| 108 | 6C | 0110 1100 |
| 109 | 6D | 0110 1101 |
| 110 | 6E | 0110 1110 |
| 111 | 6F | 0110 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 112 | 70 | 0111 0000 |
| 113 | 71 | 0111 0001 |
| 114 | 72 | 0111 0010 |
| 115 | 73 | 0111 0011 |
| 116 | 74 | 0111 0100 |
| 117 | 75 | 0111 0101 |
| 118 | 76 | 0111 0110 |
| 119 | 77 | 0111 0111 |
| 120 | 78 | 0111 1000 |
| 121 | 79 | 0111 1001 |
| 122 | 7A | 0111 1010 |
| 123 | 7B | 0111 1011 |
| 124 | 7C | 0111 1100 |
| 125 | 7D | 0111 1101 |
| 126 | 7E | 0111 1110 |
| 127 | 7F | 0111 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 128 | 80 | 1000 0000 |
| 129 | 81 | 1000 0001 |
| 130 | 82 | 1000 0010 |
| 131 | 83 | 1000 0011 |
| 132 | 84 | 1000 0100 |
| 133 | 85 | 1000 0101 |
| 134 | 86 | 1000 0110 |
| 135 | 87 | 1000 0111 |
| 136 | 88 | 1000 1000 |
| 137 | 89 | 1000 1001 |
| 138 | 8A | 1000 1010 |
| 139 | 8B | 1000 1011 |
| 140 | 8C | 1000 1100 |
| 141 | 8D | 1000 1101 |
| 142 | 8E | 1000 1110 |
| 143 | 8F | 1000 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 144 | 90 | 1001 0000 |
| 145 | 91 | 1001 0001 |
| 146 | 92 | 1001 0010 |
| 147 | 93 | 1001 0011 |
| 148 | 94 | 1001 0100 |
| 149 | 95 | 1001 0101 |
| 150 | 96 | 1001 0110 |
| 151 | 97 | 1001 0111 |
| 152 | 98 | 1001 1000 |
| 153 | 99 | 1001 1001 |
| 154 | 9A | 1001 1010 |
| 155 | 9B | 1001 1011 |
| 156 | 9C | 1001 1100 |
| 157 | 9D | 1001 1101 |
| 158 | 9E | 1001 1110 |
| 159 | 9F | 1001 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 160 | A0 | 1010 0000 |
| 161 | A1 | 1010 0001 |
| 162 | A2 | 1010 0010 |
| 163 | A3 | 1010 0011 |
| 164 | A4 | 1010 0100 |
| 165 | A5 | 1010 0101 |
| 166 | A6 | 1010 0110 |
| 167 | A7 | 1010 0111 |
| 168 | A8 | 1010 1000 |
| 169 | A9 | 1010 1001 |
| 170 | AA | 1010 1010 |
| 171 | AB | 1010 1011 |
| 172 | AC | 1010 1100 |
| 173 | AD | 1010 1101 |
| 174 | AE | 1010 1110 |
| 175 | AF | 1010 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 176 | B0 | 1011 0000 |
| 177 | B1 | 1011 0001 |
| 178 | B2 | 1011 0010 |
| 179 | B3 | 1011 0011 |
| 180 | B4 | 1011 0100 |
| 181 | B5 | 1011 0101 |
| 182 | B6 | 1011 0110 |
| 183 | B7 | 1011 0111 |
| 184 | B8 | 1011 1000 |
| 185 | B9 | 1011 1001 |
| 186 | BA | 1011 1010 |
| 187 | BB | 1011 1011 |
| 188 | BC | 1011 1100 |
| 189 | BD | 1011 1101 |
| 190 | BE | 1011 1110 |
| 191 | BF | 1011 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 192 | C0 | 1100 0000 |
| 193 | C1 | 1100 0001 |
| 194 | C2 | 1100 0010 |
| 195 | C3 | 1100 0011 |
| 196 | C4 | 1100 0100 |
| 197 | C5 | 1100 0101 |
| 198 | C6 | 1100 0110 |
| 199 | C7 | 1100 0111 |
| 200 | C8 | 1100 1000 |
| 201 | C9 | 1100 1001 |
| 202 | CA | 1100 1010 |
| 203 | CB | 1100 1011 |
| 204 | CC | 1100 1100 |
| 205 | CD | 1100 1101 |
| 206 | CE | 1100 1110 |
| 207 | CF | 1100 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 208 | D0 | 1101 0000 |
| 209 | D1 | 1101 0001 |
| 210 | D2 | 1101 0010 |
| 211 | D3 | 1101 0011 |
| 212 | D4 | 1101 0100 |
| 213 | D5 | 1101 0101 |
| 214 | D6 | 1101 0110 |
| 215 | D7 | 1101 0111 |
| 216 | D8 | 1101 1000 |
| 217 | D9 | 1101 1001 |
| 218 | DA | 1101 1010 |
| 219 | DB | 1101 1011 |
| 220 | DC | 1101 1100 |
| 221 | DD | 1101 1101 |
| 222 | DE | 1101 1110 |
| 223 | DF | 1101 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 224 | E0 | 1110 0000 |
| 225 | E1 | 1110 0001 |
| 226 | E2 | 1110 0010 |
| 227 | E3 | 1110 0011 |
| 228 | E4 | 1110 0100 |
| 229 | E5 | 1110 0101 |
| 230 | E6 | 1110 0110 |
| 231 | E7 | 1110 0111 |
| 232 | E8 | 1110 1000 |
| 233 | E9 | 1110 1001 |
| 234 | EA | 1110 1010 |
| 235 | EB | 1110 1011 |
| 236 | EC | 1110 1100 |
| 237 | ED | 1110 1101 |
| 238 | EE | 1110 1110 |
| 239 | EF | 1110 1111 |
| Decimal | Hexadecimal | Binário |
|---|---|---|
| 240 | F0 | 1111 0000 |
| 241 | F1 | 1111 0001 |
| 242 | F2 | 1111 0010 |
| 243 | F3 | 1111 0011 |
| 244 | F4 | 1111 0100 |
| 245 | F5 | 1111 0101 |
| 246 | F6 | 1111 0110 |
| 247 | F7 | 1111 0111 |
| 248 | F8 | 1111 1000 |
| 249 | F9 | 1111 1001 |
| 250 | FA | 1111 1010 |
| 251 | FB | 1111 1011 |
| 252 | FC | 1111 1100 |
| 253 | FD | 1111 1101 |
| 254 | FE | 1111 1110 |
| 255 | FF | 1111 1111 |
Olha com atenção para a coluna do binário. Cada símbolo hexadecimal corresponde exatamente a 4 bits. O 0 é sempre 0000. O F é sempre 1111. O A é sempre 1010. Essa correspondência não muda nunca, independente do contexto ou da posição do número.
E é exatamente isso que torna a conversão mecânica. Você não precisa calcular nada. Não tem conta. Não tem divisão. Não tem multiplicação. Você olha para um símbolo hexadecimal e sabe imediatamente quais 4 bits ele representa. Você olha para 4 bits e sabe imediatamente qual símbolo hexadecimal é. Para um computador fazer automaticamente agora era mais fácil. Não era mais necessário fazer centenas de contas para entregar o resultado convertido em decimal. Se você quisesse o resultado em hexadecimal, o computador só precisaria fazer substituições.
Como converter na prática
Converter um byte de binário para decimal é um processo de dois passos:
- Divide o byte em dois grupos de 4 bits.
- Consulta a tabela e substitui cada grupo pelo símbolo correspondente.
Vamos usar o primeiro byte daquele memory dump no início da seção: 01011001. Lembra dele? Em decimal, você precisou de 8 multiplicações e 8 somas para descobrir que era o número 89, com risco silencioso em cada uma das operações.
Em hexadecimal:
- Divide:
01011001vira0101 1001. - Consulta o equivalente em hexadecimal:
0101vira5.1001vira9.
- Resulado em hexadecimal: 59.
| Binário | Decimal | Hexadecimal |
|---|---|---|
| 01011001 | 89 | 59 |
Viu? É muito simples! Dois passos. Dois segundos. Praticamente zero risco de erros silenciosos.
O caminho de volta, de hexadecimal para binário é igualmente fácil. Você só faz o processo ao contrário:
- Divide:
59vira59. - Consulta o equivalente em hexadecimal:
5vira0101.9vira1001.
- Resultado em binário:
01011001.
É só isso. Não tem mais nada escondido. Toda complexidade que existia na conversão decimal simplesmente desaparece, substituída por uma tabela que, com um pouco de prática, você passa a fazer de cabeça sem nem precisar olhar para ela.
Relendo o memory dump
Agora que você sabe como o hexadecimal funciona, vamos voltar àquele memory dump do início da seção. Lembra dele? Aquele que você recebeu depois de um café da manhã, cheio de zeros e uns.
Aqui está o mesmo dump, mas desta vez em hexadecimal:
59 6F 75 20 61 72 65 20 6C 65 61 72 6E 69 6E 67
20 52 75 73 74 20 77 69 74 68 20 74 68 65 20 62
65 73 74 20 72 65 73 6F 75 72 63 65 20 65 76 65
72 3A 20 55 6C 74 69 6D 61 74 65 20 52 75 73 74
20 28 68 74 74 70 73 3A 2F 2F 75 72 75 73 74 2E
6F 72 67 29 2E 20 42 65 6C 6F 77 20 69 73 20 61
6E 20 65 78 61 6D 70 6C 65 20 6F 66 20 61 20 63
6F 72 72 75 70 74 65 64 20 74 65 78 74 20 74 68
61 74 20 70 72 6F 67 72 61 6D 6D 65 72 73 20 73
6F 6D 65 74 69 6D 65 73 20 68 61 76 65 20 74 6F
20 64 65 62 75 67 3A 20 EF BF BD EF BF BDCaso você não se lembre, ele era assim em binário
01011001 01101111 01110101 00100000 01100001 01110010 01100101 00100000 01101100 01100101 01100001 01110010 01101110 01101001 01101110 01100111
00100000 01010010 01110101 01110011 01110100 00100000 01110111 01101001 01110100 01101000 00100000 01110100 01101000 01100101 00100000 01100010
01100101 01110011 01110100 00100000 01110010 01100101 01110011 01101111 01110101 01110010 01100011 01100101 00100000 01100101 01110110 01100101
01110010 00111010 00100000 01010101 01101100 01110100 01101001 01101101 01100001 01110100 01100101 00100000 01010010 01110101 01110011 01110100
00100000 00101000 01101000 01110100 01110100 01110000 01110011 00111010 00101111 00101111 01110101 01110010 01110101 01110011 01110100 00101110
01101111 01110010 01100111 00101001 00101110 00100000 01000010 01100101 01101100 01101111 01110111 00100000 01101001 01110011 00100000 01100001
01101110 00100000 01100101 01111000 01100001 01101101 01110000 01101100 01100101 00100000 01101111 01100110 00100000 01100001 00100000 01100011
01101111 01110010 01110010 01110101 01110000 01110100 01100101 01100100 00100000 01110100 01100101 01111000 01110100 00100000 01110100 01101000
01100001 01110100 00100000 01110000 01110010 01101111 01100111 01110010 01100001 01101101 01101101 01100101 01110010 01110011 00100000 01110011
01101111 01101101 01100101 01110100 01101001 01101101 01100101 01110011 00100000 01101000 01100001 01110110 01100101 00100000 01110100 01101111
00100000 01100100 01100101 01100010 01110101 01100111 00111010 00100000 11101111 10111111 10111101 11101111 10111111 10111101Dica
Não se sinta assustado com a feiura toda que o amontoado de hexadecimais e binários parecem ser. A feiura faz parecer complexo, mas não é. Você não precisa olhar para isso e saber de cara o que significa. Ninguém sabe, não se preocupe.
O que acontece de fato é tudo que já aprendemos até aqui - você precisaria pegar cada byte (agora representado por grupos de dois hexadecimais), verificar na tabela qual a sua equivalência, e só depois aó sim você saberia o que significa. É assim mesmo.
É o mesmo conteúdo de antes. Mas olha a diferença. Cada byte agora ocupa exatamente 2 caracteres.
Os espaços repetem muitas vezes - você consegue ver o 20 aparecendo várias vezes, e se você cruzar com a tabela ASCII da seção 1.5, vai reconhecer: 20 é 32 em decimal, que é o espaço. Sem fazer nenhuma conta, só consultando a tabela hexadecimal que você acabou de aprender. No final você consegue reparar EF BF BD aparecendo duas vezes, e se você fosse um programador com um pouco mais de experiência em ler hexadecimal, você iria acabar aprendendo que esses caracteres representam um código unicode para erro - aquele símbolo � que aparece quando um texto está corrompido. Você consegue ver isso de bater o olho, sem multiplicação, sem divisão.
Um programador experiente olhando para esse dump consegue extrair informações em segundos. Não é porque é um gênio. É pelo mesmo motivo pelo qual um médico que passa anos lendo eletrocardiogramas consegue bater um olho numa linha e já sentir que algo está errado, ou um músico que lê partituras há décadas consegue ouvir a música na cabeça sem tocar uma única nota, ou um contador experiente que olha para uma coluna de números e percebe que algo não fecha antes mesmo de somar qualquer coisa. Nenhum deles nasceu sabendo. O hexadecimal torna esses padrões visíveis o suficiente para o cérebro fazer o seu trabalho.
Hexadecimal está em todo lugar
O hexadecimal nasceu da necessidade de tornar memory dumps legíveis nos anos 1960. Mas resolveu o problema tão bem que sobreviveu até hoje e se tornou a forma padrão de representar qualquer valor que precise ficar próximo do nível dos bytes - não só em depuração, mas em toda programação moderna. Você provavelmente já esbarrou nele várias vezes sem saber o que era. Vamos ver onde.
As cores
Olhe para a tela que você está usando agora. Pode ser um monitor, um celular, um tablet - não importa. O que todas essas telas têm em comum é que a imagem que você vê é formada por milhões de pontinhos minusculos chamados pixels. Cada pixel é um quadradinho tão pequeno que você não consegue ver individualmente a olho nu, mas juntos eles formam tudo que aparece na tela.
Cada pixel não é uma coisa só. Cada pixel é formado por três luzes microscópicas, uma ao lado da outra - uma vermelha, uma verde, e uma azul. O que você enxerga como "uma cor" é na verdade a mistura dessas três luzes com intensidades diferentes. Esse modelo se chama RGB, do inglês Red, Green, Blue, que significa literalmente Vermelho, Verde, Azul.
Parece confuso? Pense assim: se você ligar o vermelho no máximo, o verde no máximo e o azul no máximo, as três luzes juntas formam branco. Se você desligar as três, você tem preto. Se você ligar só o vermelho no máximo e deixar os outros dois desligados, você tem vermelho puro. E variando a intensidade das três luzes, você consegue produzir praticamente qualquer cor que o olho humano consegue distinguir.
Mas ligar no máximo e desligar são apenas os extremos. Cada componente pode ter qualquer intensidade entre o 0 e o máximo. E a pergunta que pode aparecer é: quantos níveis de intensidade usar?
Agora que você já entende como informação funciona dentro do computador, vai ficar bem fácil entender essa. Cada pixel tem cada cor armazenada em 1 byte, por tanto, cada cor pode variar entre 0 (desligada) e 255 (máximo). Cada píxel ocupa exatamente 3 bytes na memória: um para o vermelho, um para o verde e um para o azul.
256 níveis de intensidade para cada cor resulta em 256 x 256 x 256 = mais de 16 milhões de cores distintas. O olho humano consegue distinguir em torno de 10 milhões de cores. Ou seja, 256 níveis de intensidade por cor é mais do que suficiente para reproduzir qualquer cor que você seria capaz de perceber.
E é aqui que o hexadecimal entra de forma completamente amigável. Quando você vê uma cor escrita como #FF5733, você está vendo três bytes em hexadecimal, um para cada componente:
FFé a intensidade do vermelho - 255 em decimal, intensidade máxima.57é a intensidade do verde - 87 em decimal.33é a intensidade do azul - 51 em decimal.
O resultado é a cor abaixo - que você pode clicar para brincar com os tons de RGB (vermelho, verde e azul):
Toda vez que um programador precisa definir uma cor no código - seja para um site, um aplicativo, um jogo, qualquer coisa com interface visual - ela é escrita exatamente assim: três bytes em hexadecimal, precididos de #. Você vai fazer isso muitas vezes ao longo do Ultimate Rust, e com o tempo vai se tornar algo tão natural quanto escrever qualquer outro número.
A notação unicode
Na seção 1.6, você aprendeu que o Unicode atribui um número único a cada caractere de todos os idiomas e sistemas de escrita do mundo. Esse número é chamado de code point. Você já tinha visto que o A é o code point 65. O ã é o code point 227. O 🌍 é o code point 127757.
Agora, como você escreve esses números quando precisa se referir a um caractere específico quando estiver programando e o caractere não tem no seu teclado? Ou numa documentação, numa mensagem de erro, discussão técnica? Em decimal você poderia escrever 65, 227 e 127757. Mas a comunidade Unicode adotou uma notação diferente desde o início: os code points são escritos em hexadecimal, precedidos de U+.
Então o A vira U+0041. O ã vira U+00E3. O 🌍 vira U+1F30D.
Você deve estar se perguntando duas coisas. Primeira: por que em hexadecimal? E você já sabe a resposta. Hexadecimal tem uma relação direta com bytes, e code points são valores que eventualmente precisam ser representados em bytes na memória. Escrever U+00E3 em vez de U+227 não é só uma questão de gosto. Lembra que par de dígitos hexadecimais corresponde a 1 byte? Aqui é a mesma coisa. Quando você olha para U+00E3, você já está olhando para bytes por baixo - e isso vai fazer cada vez mais sentido conforme você for aprendendo como textos são armazenados na memória ao longo do Ultimate Rust.
Segunda pergunta: por que U+0041 e não simplesmente U+41? Afinal, 41 em hexadecimal é a mesma coisa que 0041 - os zeros à esquerda não mudam nada matematicamente. A razão é simplesmente por convenção. O Unicode foi projetado para acomodar mais de um milhão de caracteres, e os code points podem ter até 6 dígitos hexadecimais. Para tornar os valores comparáveis e fáceis de ler lado a lado, a convenção é sempre usar no minínmo 4 dígitos, preenchidos com zeros à esquerda quando necessário. Assim, U+0041 e U+1F30D ficam visualmente alinhados.
É o mesmo motivo pelo qual em horários os minutos são escritos como 09:05 e não 9:5. Os zeros não mudam o significado, mas tornam a leitura mais rápida e menos propensa a erros.
Você vai ver a notação U+XXXX constantemente ao longo da sua carreira. Agora você sabe exatamente o que ela significa e por que foi escolhida dessa forma.
Endereços de memória e o prefixo 0x
Toda vez que seu programa guarda um valor na memória, esse valor fica guardando em algum endereço, como você aprendeu na subseção "O computador não pensa em bits, pensa em bytes", da seção anterior. Um endereço é um número que indica exatamente onde na memória aquele dado está, como o número de uma casa numa rua. E esses endereços são sempre escritos em hexadecimal.
Quando você programar em Rust - e isso vai acontecer mais cedo do que você imagina - vai se deparar com endereços assim: 0x7FFE4B2A. Ou com constantes assim: 0xFF, 0x00E3, 0x1F30D.
O 0x que aparece antes do número é a convenção adotada por praticamente todas as linguagens de programação modernas para avisar: "o que vem a seguir está em hexadecimal". O 0x não faz parte do valor em si, tal como o U+ no Unicode e o # nas cores também não fazem. Em Rust, escrever 255 e 0xFF é exatamente a mesma coisa para a linguagem. Qual usar é uma escolha sua, que vai depender do contexto.
Essa escolha importa mais do que parece. Pense bem: você passou a seção inteira aprendendo que o problema do decimal é que ele não tem relação direta com bits. O hexadecimal resolve exatamente isso. Então quando você está escrevendo código que "trabalha próximo da memória" - e isso vai acontecer - usar 0xFF em vez de 255 não é só uma preferência estética. É uma convenção. Usar 255 funcionaria, mas quem estiver lendo o seu código espera encontrar 0xFF quando se trata de bytes realmente.
Lembra do que falamos sobre tornar o seu código mais legível para outras pessoas? Esse é um exemplo. É uma escolha que torna o código mais fácil de entender. Quem vê 0xFF sabe imediatamente, sem calcular nada, que todos os 8 bits desse byte estão ligados - porque F é 1111, e você tem 2 Fs, um para cada metade do byte. Quando você vê 255, você precisa primeiro lembrar que 255 é o valor máximo de um byte, e aí concluir que todos os bits estão ligados. São dois passos em vez de um.
Fique tranquilo!
Com o tempo e com a prática, ler hexadecimal vai se tornar cada vez mais natural, exatamente como aconteceu com a leitura das letras quando você era criança. No começo era lento e trabalhoso. Hoje você lê sem pensar. Com hexadecimal vai ser a mesma coisa.
Bytes precisam de contexto
Você agora consegue olhar para um memory dump em hexadecimal e extrair informações dele. Sabe que o 20 é um espaço, que EF BF BD é um caractere Unicode corrompido, que FF é um byte com todos os bits ligados. O hexadecimal cumpriu o que prometeu: tornou bytes legíveis.
Mas para um olho ainda não treinado, mesmo o dump em hexadecimal é só uma sequência de números e letras estranhas. E isso releva algo importante: saber ler os bytes não é suficiente. Você também precisa saber o que aqueles bytes representam, e isso não está escrito nos bytes em si.
Se eu lhe mostrar a sequência FF D8 FF EO, você consegue ler os quatro bytes perfeitamente em hexadecimal. Mas o que eles representam? Um número? Um texto? um trecho de áudio? Parte de uma imagem?
A resposta é: depende. Depende de quem criou esses bytes, com que intenção, e - mais importante - de quem está lendo e como está interpretando. Os bytes não carregam essa informação dentro deles. Eles são apenas números em algum lugar da memória. O significado vem de fora.
Esse é o ponto que o capítulo inteiro estava tentando construir sem nomear diretamente. Você aprendeu que os computadores só guardam zeros e uns. Aprendeu que zeros e uns formam bytes. Aprendeu que bytes podem representar números, letras, code points, endereços, cores. Mas em nenhum momento os bytes te disseram o que eram. Eu te disse. Você precisou de uma tabela, de uma convenção, de um contexto externo para interpretar cada um deles.
Uma foto, uma música, um documento de texto - todos são apenas sequências de bytes. O que os diferencia não está nos bytes em si. Está no contexto que existe em torno deles.
E quem gerencia esse contexto no seu computador é o assunto da próxima seção.
Antes de avançar
Você consegue responder:
- Por que o hexadecimal foi adotado para representar bytes, em vez de decimal ou binário diretamente?
- Dado o byte
10110100, como você o converteria para hexadecimal? E o caminho inverso: dadoC7, quais são os bits? - Por que bytes sozinhos não são suficientes para saber o que um dado representa?
Se consegue responder as três, está pronto para a última seção do capítulo.