Hardware e sistemas operacionais · parte 4 de 27
Texto, ASCII, Unicode e UTF-8: como uma letra vira número
Como uma letra vira número, por que o mesmo texto ocupa bytes diferentes e por que medir tamanho de strings pode enganar no dia a dia.
iniciantePublicada em · 30 min de leitura
Texto, ASCII, Unicode e UTF-8: como uma letra vira número
Você digita “João” no celular, o texto some para dentro de um servidor, volta do banco de dados e aparece na tela de outra pessoa, num navegador de outro país, sem nenhum erro de acento. Parece trivial, e é um dos milagres mais silenciosos da computação. Por trás disso existe uma convenção de que cada letra é um número, que esse número é gravado em bytes, e que todo mundo concorda com a mesma tabela. Quando alguém esquece uma dessas três coisas, a palavra “João” vira “João” na tela e ninguém entende por quê.
A gente vai começar pelo problema real, que é anterior aos computadores, e depois descer até o byte. Nesta aula você vai ver como o telégrafo resolveu o texto com um código de cinco pulsos, por que o ASCII de sete bits não bastou para o português, como o Unicode separou o “que letra é” do “como guardar”, e por que o UTF-8 virou o padrão da web. Os exemplos rodam, e a saída de cada um está colada no texto. Pré-requisito: saber o que é um bit e converter binário e hexadecimal, o que está em Bits e binário.
Mapa da aula
A aula começa pela ideia, passa pela história que a motivou, pelos exemplos e pelo mecanismo do UTF-8 e do Unicode. A seção 5 cuida das bordas: UTF-16, BOM, normalização, emoji, a ordenação de palavras e o caminho de um texto digitado até a tela. Os erros comuns e os exercícios vêm depois, seguidos do glossário e das fontes.
1. Uma letra é um número
Pense em uma sala de aula em que cada aluno tem um número na chamada. A professora não grava o nome de ninguém no caderno: ela escreve o número, e quem sabe a chamada consegue voltar do número para o nome. Um computador faz o mesmo com o texto. A letra “A” é guardada como o número 65, a letra “a” como 97, o espaço como 32. Quem lê o arquivo precisa conhecer a chamada, ou seja, a tabela que diz qual número é qual letra. A chamada, porém, só diz qual número é qual letra. Não diz como esse número é escrito no caderno, e essa parte é a codificação, que a aula explica mais adiante.
Tecnicamente, o conjunto de caracteres que uma convenção reconhece se chama repertório, e cada caractere recebe um número inteiro chamado ponto de código. A forma de gravar esses números em bytes se chama codificação. Essa separação é a chave da aula. O ponto de código responde “que caractere é este”. A codificação responde “como eu guardo esse número em bytes”. Quando as duas coisas se misturam na cabeça, aparecem quase todos os bugs de texto.
Um byte tem oito bits e cobre 256 valores, de 0 a 255. Se cada caractere coubesse em um byte, o texto seria simples, e o problema seria só de tamanho da tabela. O que torna a história longa é que o mundo tem muito mais que 256 caracteres, e que os primeiros sistemas precisavam caber em muito menos.
2. Para que serve: o telégrafo e o código de cinco pulsos
Antes do computador, a transmissão de texto à distância era telégrafo. Na década de 1870, Émile Baudot criou um código em que cada caractere era transmitido com cinco pulsos elétricos. A máquina dele foi patenteada em 1874, mas o código em si não foi patenteado. Cinco bits dão 32 combinações (2 elevado a 5). Não é o bastante para 26 letras, dez dígitos e a pontuação ao mesmo tempo, então o equipamento alterna entre um modo de letras e um modo de figuras, e a mesma combinação de pulsos significa coisas diferentes em cada modo. Essa troca de modo é a primeira vez que aparece nesta aula a ideia de que o significado de um código depende do contexto. A história também tem uma disputa de prioridade: Louis Victor Mimault levou o caso à justiça em 1877, e a corte chegou a dar razão a ele antes de a decisão ser revista (fonte: verbete Baudot code da Wikipedia, consultado em 1 de novembro de 2026, ver Fontes).
A figura abaixo resume a sequência de codificações que vieram depois. Cada uma surgiu porque a anterior não cabia no que as pessoas precisavam escrever.
A figura lê da esquerda para a direita. O ASCII, de 1963, usa sete bits e 128 posições, mas a edição de 1963 não tinha letras minúsculas: elas só entraram na revisão de 1967, e por isso o inglês normal só fica completo a partir dela. Quando computadores de outros países passaram a usar o mesmo sistema, cada um precisou de letras com acento, e apareceram as páginas de código, tabelas diferentes que reaproveitavam as mesmas posições para letras distintas. O Unicode surgiu para acabar com essa confusão, e o UTF-8 é a forma de gravar o Unicode que a web e a maioria dos sistemas usam hoje.
O ASCII merece uma olhada de perto porque ainda está em todo lugar. A edição de 1963, publicada como ASA X3.4-1963, deixou 28 posições sem significado atribuído, e as letras minúsculas foram definidas na revisão de 1967, publicada como USAS X3.4-1967 (fontes: verbete ASCII da Wikipedia e ascii-code.com, ambos consultados em 1 de novembro de 2026; o texto original da norma não foi aberto). O comitê escolheu sete bits para reduzir o custo da transmissão. O oitavo bit ficou livre, e podia servir para verificação de erro por paridade. Quando as páginas de código passaram a usar esse bit, elas ocuparam as posições de 128 a 255 para as letras acentuadas. A figura seguinte mostra a letra A em sete bits, e também a letra é, que tem o número 233 e portanto precisa do oitavo bit.
O oitavo bit sozinho já divide o mundo. Com ele zerado, o byte é ASCII e cabe nas 128 posições do padrão. Com ele aceso, o byte passa a representar uma tabela diferente, e a mesma sequência de bits pode significar um acento em uma página de código e outro símbolo em outra. Essa ambiguidade é o motivo de quase toda a confusão que vem a seguir.
3. Exemplos em contextos diferentes
O primeiro contexto é o da língua. Para escrever português, você precisa de ç, ã, é e de dezenas de outros caracteres. Quando um sistema grava “João” em uma codificação e lê em outra, cada byte acentuado vira outro caractere. A figura 3 mostra o caso clássico, o mojibake (nome que se dá ao texto que aparece com caracteres trocados), em que os bytes de “João” em UTF-8 são lidos como se fossem Latin-1. Latin-1 (ISO 8859-1) é uma tabela de um byte por caractere, com as 128 primeiras posições iguais ao ASCII e as demais ocupadas por letras acentuadas de línguas da Europa ocidental. A seção 4 mostra a tabela em detalhe.
O segundo contexto é o do banco de dados. Se a conexão com o banco usa uma codificação e a aplicação espera outra, o texto que entra e o que sai não batem. Um exemplo: a aplicação grava “João” em UTF-8, o banco guarda os cinco bytes, e a aplicação lê esses bytes como Latin-1 e mostra “João” para o usuário, numa tela que parece correta por fora. O terceiro contexto é o da rede: um navegador decide como ler a página pelo cabeçalho de resposta ou pela marcação do próprio documento. Se nenhum dos dois informa a codificação, o navegador usa uma heurística, uma regra de palpite que pode errar.
O quarto contexto é o emoji. Um emoji de rosto ocupa quatro bytes em UTF-8, e algumas figuras, como a família, são compostas de vários pontos de código. A seção 5 volta a esse caso com números medidos. Por enquanto, guarde que o que você digita nem sempre corresponde a um só número.
4. Como funciona por dentro: Unicode e UTF-8
O Unicode é o repertório. Ele atribui a cada caractere um ponto de código escrito no formato U+ seguido de hexadecimal. A letra é tem o ponto U+00E9, o euro tem U+20AC, e o emoji de rosto sorrindo tem U+1F600. O espaço de pontos de código vai de U+0000 até U+10FFFF, o que dá 1.114.112 posições, organizadas em 17 planos de 65.536 cada. Plano é um bloco de 65.536 pontos de código, numerado de 0 a 16. Nem todas as posições têm um caractere.
A versão 17.0 do Unicode foi anunciada em 9 de setembro de 2025 e acrescenta 4.803 caracteres gráficos (fonte: blog.unicode.org, ver Fontes). A página de estatísticas da versão traz números que se confundem com facilidade. O número de caracteres gráficos (letras, símbolos e marcas de acento) mais os de formato, que são invisíveis mas têm função, dá 159.801. Esse é o número de caracteres. Logo abaixo aparece “Total Assigned”, com 297.334. Esse total não é só de caracteres: ele soma os 159.801 acima, 137.468 pontos de uso privado (posições que cada programa pode usar como quiser, sem que o padrão dê nome a elas) e 65 controles, como a quebra de linha. A conta fecha: 159.629 + 172 + 65 + 137.468 = 297.334. Os 2.048 substitutos (de D800 a DFFF, usados só para montar pares no UTF-16) e os 66 não-caracteres (posições reservadas para uso interno de programas) ficam de fora desse total. Sobram 814.664 pontos reservados, que ainda não têm uso.
O UTF-8 é a codificação desse repertório em bytes. Ele tem uma regra simples que cabe em uma tabela. Um ponto de código entre U+0000 e U+007F ocupa um byte, com o bit alto zerado. Entre U+0080 e U+07FF ocupa dois bytes. Entre U+0800 e U+FFFF ocupa três, e entre U+10000 e U+10FFFF ocupa quatro. O primeiro byte de cada sequência anuncia quantos bytes vêm, por meio dos seus primeiros bits: 0 para um byte, 110 para dois, 1110 para três e 11110 para quatro. Os bytes seguintes sempre começam com 10, e são chamados de bytes de continuação.
A figura abaixo desmonta os três exemplos de bits. A letra é ocupa dois bytes, o euro três e o emoji quatro. Os campos cinza são os prefixos, que dizem o tamanho, e os verdes são a carga, que guarda os bits do ponto de código. Repare que o total de bits de carga é o que importa: 11 para é, 16 para o euro e 21 para o emoji.
Leia a figura pela primeira linha. A letra é é U+00E9, que tem o valor decimal 233 e em binário é 11101001. Com zeros à esquerda para completar 11 bits, o número fica 00011101001. Esses 11 bits se encaixam em dois bytes: o primeiro leva 110 de prefixo e cinco bits de carga, e o segundo leva 10 de prefixo e seis bits de carga. O resultado, 110 00011 e 10 101001, dá os bytes C3 A9. O mesmo método funciona para o euro. U+20AC é 0010 000010 101100 em 16 bits, que se dividem em 4, 6 e 6 bits, e o resultado é E2 82 AC. O emoji tem 21 bits, 0 0001 1111 0110 0000 0000, que se dividem em 3, 6, 6 e 6, e o resultado é F0 9F 98 80. Os três resultados são os que o Python grava: 'é'.encode('utf-8') dá c3 a9, '€'.encode('utf-8') dá e2 82 ac e o emoji dá f0 9f 98 80.
A regra do UTF-8 tem uma propriedade que vale a pena guardar: a autossincronização. Se você cair no meio de um caractere, basta recuar até achar um byte que não comece com 10. Esse byte é o início do caractere onde você caiu, e a partir dele a decodificação recomeça sem ler o arquivo inteiro. Por exemplo, se você cair no a9 de c3 a9, recua um byte, encontra o c3, que começa com 11, e sabe que ali começa o é. Essa propriedade é o que permite a um programa pular para uma posição do texto e continuar decodificando sem se perder.
Também vale a compatibilidade com ASCII. Qualquer byte abaixo de 128 é um caractere ASCII com o mesmo significado, e por isso um arquivo só de inglês em UTF-8 é byte a byte idêntico ao mesmo arquivo em ASCII. Essa foi uma das decisões que tornou o UTF-8 aceitável para o mundo inteiro. Ken Thompson projetou o UTF-8 em 1992, com Rob Pike acompanhando o trabalho. Pike conta, em seu relato, que a ideia foi desenhada num jogo americano de papel de uma lanchonete em New Jersey, numa noite de setembro de 1992 ou por aí (tradução livre; fonte: transcrição do relato feita por Markus Kuhn, ver Fontes). Uma busca no arquivo de mensagens, feita por Russ Cox, encontra o primeiro rascunho datado de 2 de setembro de 1992. A aula não depende do dia exato. A especificação oficial é a RFC 3629, de novembro de 2003, que restringiu o UTF-8 ao intervalo do Unicode e proibiu os substitutos (fonte: rfc-editor.org, ver Fontes).
A figura seguinte mostra as três codificações da mesma palavra “João”, lado a lado. O ã ocupa um byte em Latin-1 (E3) e dois em UTF-8 (C3 A3). Em UTF-16 big endian, cada letra ocupa uma unidade de 16 bits, ou seja, 2 bytes. Por isso o texto passa de 4 bytes em Latin-1 ou 5 em UTF-8 para 8 bytes em UTF-16.
Latin-1 e cp1252 merecem uma definição à parte, porque a mesma sequência de bytes pode significar coisas diferentes nas duas. Latin-1 (ISO 8859-1) tem 256 posições de um byte cada, e não tem o euro. A página de código cp1252, do Windows, usa posições de 128 a 159, que em Latin-1 são controles invisíveis, para o euro e outros sinais. O programa a seguir, rodado com Python 3.12.8, mostra a diferença. “é” vira o byte E9 em Latin-1, e ”€” vira 80 em cp1252, enquanto ”€” em Latin-1 dá erro. A última linha mostra o mojibake do euro: os três bytes UTF-8 do € lidos como cp1252 viram três caracteres.
print("'é' em latin-1 :", "é".encode("latin-1"))
print("'€' em cp1252 :", "€".encode("cp1252"))
try:
"€".encode("latin-1")
except UnicodeEncodeError as e:
print("'€' em latin-1 : erro ->", e.reason)
print("João em UTF-8 lido como latin-1:", "João".encode("utf-8").decode("latin-1"))
print("euro UTF-8 lido como cp1252 :", "€".encode("utf-8").decode("cp1252"))
'é' em latin-1 : b'\xe9'
'€' em cp1252 : b'\x80'
'€' em latin-1 : erro -> ordinal not in range(256)
João em UTF-8 lido como latin-1: João
euro UTF-8 lido como cp1252 : €
Sem saber qual das duas foi usada, o mesmo byte pode virar um caractere ou outro, e isso é mais uma fonte de mojibake.
O decodificador é o programa que faz o caminho inverso: recebe bytes e devolve pontos de código. A figura seguinte mostra a decisão dele. Antes de contar os bytes que faltam, o decodificador recusa os bytes que nunca podem começar um caractere: 10xxxxxx, que só aparece como continuação, C0 e C1, que codificariam em dois bytes um valor que cabe em um, e F5 a FF, que passariam de U+10FFFF. Depois, o primeiro byte diz quantas continuações esperar (110, 1110 ou 11110). Cada continuação precisa começar com 10, e o valor montado precisa estar no intervalo válido, sem sobras e sem substitutos. Se algo falha, o decodificador devolve U+FFFD em vez de quebrar. A figura mostra essa decisão e essa validação, não tudo o que um decodificador de produção faz: por exemplo, ela não cobre o fim do fluxo no meio de um caractere.
Para ver o fluxo em um caso válido, siga o é, que é c3 a9. O byte c3 começa com 110, então faltam 1 byte de continuação. O a9 começa com 10, então entra com seus seis bits, 101001. Juntando 00011 com 101001, o valor é 00011101001, que é U+00E9. Esse valor é válido, e o caractere é emitido. O programa Go, mais adiante nesta seção, mostra o caso de um byte inválido, o FF, que vira U+FFFD.
5. Detalhes e bordas
Com o UTF-8 entendido, as bordas ficam mais fáceis. A primeira é o UTF-16, usado pelo Java, pelo JavaScript e internamente pelo Windows. Ele grava cada unidade em 16 bits. Os pontos do plano básico (até U+FFFF) ocupam uma unidade, e os do resto do mundo ocupam duas, com os chamados substitutos: um par de unidades entre D800 e DFFF que, juntas, representam um ponto acima de U+FFFF. O intervalo D800 a DFFF é reservado para isso e não representa caractere nenhum sozinho. A conta do emoji de rosto mostra como o par é montado. U+1F600 menos U+10000 dá 0xF600. Os 10 bits altos desse número (0x3D) somados a D800 dão D83D, e os 10 bits baixos (0x200) somados a DC00 dão DE00. Por isso o emoji vira o par D83D DE00.
A figura abaixo mostra o mesmo emoji em UTF-16 com a marca de ordem de bytes (BOM, U+FEFF) no início, em big endian e em little endian. Big endian grava o byte mais significativo primeiro, e little endian grava o menos significativo primeiro. A diferença aparece só na ordem dos bytes dentro de cada unidade, e o BOM é o que permite ao leitor descobrir qual das duas ordens foi usada.
O BOM serve para o leitor descobrir a ordem dos bytes. Ele é útil em UTF-16, onde big e little endian são ambíguos, e é desnecessário em UTF-8, que não tem ordem de bytes. Um BOM no início de um arquivo UTF-8 é um sinal de herança de programas antigos, e alguns leitores o mostram como um caractere invisível que estraga o primeiro campo de um CSV. Se você recebe um arquivo assim, o sintoma é um cabeçalho que não bate com o nome da coluna.
A segunda borda é a normalização. A letra é pode ser escrita de duas formas: como o ponto único U+00E9, ou como a letra e seguida do acento combinante U+0301. As duas são o mesmo caractere para o olho, e são sequências diferentes para o computador. Por isso a comparação de strings falha e a busca não encontra o nome. A figura mostra as duas formas, chamadas NFC (composta) e NFD (decomposta), com os números de cada uma.
Os números abaixo, rodados em Python, mostram o problema com clareza. Os dois literais usam escapes (é e é) para que a diferença fique visível no código. A função unicodedata.normalize converte uma forma na outra, mas nenhuma das duas é mais correta que a outra. A escolha precisa ser feita de propósito, e a maioria dos sistemas usa NFC.
import unicodedata
pre = "é" # é como um ponto de código só (NFC)
dec = "é" # e + acento combinante U+0301 (NFD)
print("pre == dec ?", pre == dec, "| len pre =", len(pre), "| len dec =", len(dec))
print("NFD(pre) == dec ?", unicodedata.normalize("NFD", pre) == dec)
print("len NFC(dec) =", len(unicodedata.normalize("NFC", dec)))
pre == dec ? False | len pre = 1 | len dec = 2
NFD(pre) == dec ? True
len NFC(dec) = 1
A terceira borda é o grafema, o que o olho vê como um caractere. Um grafema pode ser formado por vários pontos de código. A família de emoji é um exemplo: três pessoas unidas por dois caracteres invisíveis chamados ZWJ (joiner de largura zero, U+200D). O terminal que sabe juntar esses pontos mostra uma figura. O que não sabe mostra as três pessoas separadas, ou um quadrado. A figura abaixo mostra a conta.
Conte as figuras para entender o padrão. Três pessoas são três pontos, e os dois ZWJ são dois pontos, então o resultado tem cinco pontos de código. Em UTF-8, cada pessoa ocupa 4 bytes e cada ZWJ ocupa 3, e a conta dá 3 × 4 + 2 × 3 = 18 bytes. Em UTF-16, as pessoas ocupam 2 unidades cada e o ZWJ ocupa 1, o que dá 3 × 2 + 2 × 1 = 8 unidades. Um texto como ”👍🏽” mostra o mesmo fenômeno com menos pontos: o polegar mais o modificador de pele, que é um ponto separado, compõem um grafema só. A figura abaixo mostra esse caso com as três camadas lado a lado.
Quem precisa contar caracteres para cortar um texto em uma interface deve contar grafemas, e não pontos. A biblioteca padrão do JavaScript faz isso com Intl.Segmenter.
O quarto ponto é a comparação e a ordenação. Ordenar por ponto de código coloca “Z” antes de “a” e “é” depois de “zebra”, porque o número de “é” é maior que o de “z”. A ordenação que o português espera vem de regras de collation (regras de ordenação de um idioma, que dizem como comparar letras acentuadas e maiúsculas). O programa de JavaScript a seguir mostra a diferença entre as duas ordens. O Array.sort() do JavaScript compara por unidade UTF-16, e para letras do plano básico isso coincide com o ponto de código.
const palavras = ["zebra", "é", "e", "ê", "Z", "a"];
console.log("codigo:", [...palavras].sort());
console.log("pt-BR :", [...palavras].sort(new Intl.Collator("pt-BR").compare));
codigo: [ 'Z', 'a', 'e', 'zebra', 'é', 'ê' ]
pt-BR : [ 'a', 'e', 'é', 'ê', 'Z', 'zebra' ]
Os dois resultados são corretos, cada um para o seu critério. O erro está em usar o primeiro quando o usuário espera o segundo, o que acontece em qualquer lista ordenada que aparece na tela.
Por fim, a comparação por código parece simples, e é a causa de um bug silencioso. O Go compara strings byte a byte, e o byte de “z” (0x7A) é menor que o primeiro byte de “é” (0xC3), por isso “z” < “é” é verdadeiro, e “é” < “z” é falso. A linha final da saída do programa Go, mais abaixo, mostra isso.
A figura seguinte mostra a conta de tamanhos em quatro linguagens, com o mesmo texto. As barras contam unidades de cada linguagem, e não medem desempenho.
Esse gráfico tem uma lição prática. Quando você escreve len(s) em Python, s.length em JavaScript ou len(s) em Go, cada um responde uma pergunta diferente: pontos de código, unidades UTF-16 e bytes. O número é correto para cada linguagem e errado se você esperava outra coisa. Os valores do gráfico foram medidos com Go 1.27.2, OpenJDK 21.0.8, Python 3.12.8 e Node v24.21.0. Em Python, len("👍🏽") dá 2; em JavaScript, "👍🏽".length dá 4. O programa em Go a seguir mede os tamanhos e também mostra o byte inválido e as comparações de strings.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
casos := []string{"a", "é", "€", "😀", "João", "👍🏽", "👨👩👧"}
for _, s := range casos {
fmt.Printf("%-18q bytes=%2d runas=%d\n", s, len(s), utf8.RuneCountInString(s))
}
r, tam := utf8.DecodeRuneInString(string([]byte{0xFF}))
fmt.Printf("byte FF: runa=%q U+%04X tamanho=%d\n", r, r, tam)
fmt.Println(`"z" < "é" ?`, "z" < "é")
fmt.Println(`"é" < "z" ?`, "é" < "z")
}
"a" bytes= 1 runas=1
"é" bytes= 2 runas=1
"€" bytes= 3 runas=1
"😀" bytes= 4 runas=1
"João" bytes= 5 runas=4
"👍🏽" bytes= 8 runas=2
"👨👩👧" bytes=18 runas=5
byte FF: runa='�' U+FFFD tamanho=1
"z" < "é" ? true
"é" < "z" ? false
Repare que o Go é o único dos quatro em que len responde bytes. Isso é consistente com a especificação da linguagem, que define o tamanho de uma string em bytes e a iteração com range em runas (runa é o nome que o Go dá a um ponto de código). O byte FF não começa nenhum caractere válido, e o Go o devolve como U+FFFD, com tamanho 1. A função utf8.RuneCountInString conta as runas.
Em Java, a mesma conta sai assim, com String.length contando unidades UTF-16 e codePointCount contando pontos de código. O programa fica como segue. A linha do família mostra o ZWJ escapado, porque um caractere invisível não se lê na tela.
import java.nio.charset.StandardCharsets;
public class Texto {
public static void main(String[] args) {
String[] casos = {"a", "é", "€", "😀", "João", "👍🏽", "👨👩👧"};
for (String s : casos) {
String mostra = s.replace("", "\\u200d");
System.out.printf("%-20s length=%d pontos=%d bytes UTF-8=%d%n",
mostra, s.length(), s.codePointCount(0, s.length()),
s.getBytes(StandardCharsets.UTF_8).length);
}
}
}
a length=1 pontos=1 bytes UTF-8=1
é length=1 pontos=1 bytes UTF-8=2
€ length=1 pontos=1 bytes UTF-8=3
😀 length=2 pontos=1 bytes UTF-8=4
João length=4 pontos=4 bytes UTF-8=5
👍🏽 length=4 pontos=2 bytes UTF-8=8
👨👩👧 length=8 pontos=5 bytes UTF-8=18
Por último, o caminho do texto. Um texto digitado passa por várias camadas, e cada uma pode reinterpretá-lo. A figura mostra a sequência do teclado ao navegador, com a marca de cada etapa.
Veja a última mensagem da figura, que é o ponto em que a maioria dos erros começa. O navegador precisa saber que a resposta está em UTF-8. Esse aviso vem do cabeçalho Content-Type, que o servidor web envia, ou da marcação do próprio documento. Sem ele, o navegador tenta adivinhar, e a adivinhação falha com textos curtos e com acentos de idiomas diferentes.
6. Erros comuns e boas práticas
O primeiro erro é tratar len como número de caracteres. Ele conta bytes em Go, unidades UTF-16 em Java e JavaScript, e pontos de código em Python. Para cortar um texto na tela, conte grafemas. Para o limite de tamanho de um campo no banco, a pergunta é em que unidade o banco mede. MySQL e PostgreSQL contam caracteres em VARCHAR(n), enquanto o Oracle aceita VARCHAR2(n BYTE), que conta bytes. Com essa definição, “João” tem 5 bytes em UTF-8 e não cabe numa coluna VARCHAR2(4 BYTE), mesmo tendo quatro letras. Quem copia um tamanho de um banco para outro sem conferir a unidade costuma descobrir o erro só com um sobrenome acentuado.
O segundo erro é misturar codificações sem avisar. Um arquivo salvo em Latin-1 e lido como UTF-8 dá um erro de decodificação, e um arquivo salvo em UTF-8 e lido como Latin-1 dá o mojibake da figura 3. O conserto é declarar a codificação em cada fronteira: na leitura do arquivo, na conexão com o banco, no cabeçalho HTTP e na marcação HTML. A regra vale em cada fronteira, porque cada uma pode reinterpretar os bytes.
O terceiro erro é comparar strings sem normalizar. Dois usuários que digitam o mesmo nome, um com a letra composta e outro com a decomposta, não encontram um ao outro na busca. A boa prática é normalizar para NFC na entrada, antes de gravar ou comparar. O quarto erro, que quase todo mundo comete uma vez, é achar que UTF-16 é “o Unicode de 16 bits”. Ele é uma codificação do Unicode que usa unidades de 16 bits e substitutos para os pontos acima de U+FFFF, e por isso um emoji conta como dois em Java e JavaScript.
O quinto erro é esquecer que a ordenação depende do idioma. Ordenar uma lista de sobrenomes pelo código dá resultados que o usuário brasileiro considera errados. Use a collation do idioma, como Intl.Collator ou o equivalente do seu banco.
7. Exercícios
- Converta a palavra “maçã” para bytes UTF-8 à mão. Quantos caracteres, quantos pontos de código e quantos bytes ela tem? Depois confira com
"maçã".encode("utf-8")em Python. - Um byte da sequência é 0xB5, sozinho, sem nenhum byte antes. Pelo fluxo de decisão da figura 6, o que um decodificador deve fazer com ele? Explique com o prefixo do byte.
- Explique por que
"z" < "é"é verdadeiro em Go, mas uma lista ordenada pela collation pt-BR põe “é” antes de “z”. - Calcule o número de bytes de um texto de 100 letras do alfabeto latino sem acento, e depois de um texto com 100 emojis de rosto. Use o tamanho de cada caractere em UTF-8.
Respostas dos exercícios
- “maçã” tem quatro caracteres e quatro pontos de código: m (U+006D), a (U+0061), ç (U+00E7) e ã (U+00E3). Em UTF-8, m e a ocupam um byte cada. O ç (U+00E7) é 000 11100111 com zeros à frente, que se dividem em 00011 e 100111: 110 00011 e 10 100111, ou seja, C3 A7. O ã (U+00E3) segue o mesmo caminho e dá C3 A3. Os seis bytes são
6d 61 c3 a7 c3 a3. A resposta supõe que o ç veio na forma composta, NFC. Se o ç foi digitado decomposto (c seguido do acento combinante U+0327), a palavra tem seis pontos de código e oito bytes:6d 61 63 cc a7 61 cc 83. Por isso a normalização vem antes da contagem. - O byte 0xB5 tem os bits 10110101, que começam com 10. Como byte inicial de um caractere, o prefixo 10 é proibido, porque ele só aparece como continuação. O decodificador deve devolver U+FFFD e seguir para o próximo byte.
- Em Go, a comparação é feita byte a byte, e o primeiro byte de “é” é 0xC3, que é maior que 0x7A de “z”. Na collation pt-BR, a ordem segue o alfabeto e o acento só desempata letras iguais na base: “é” fica depois de “e” e antes de “ê”, e todas essas letras ficam antes de “z”.
- Cem letras do alfabeto latino sem acento ocupam 100 bytes, porque cada uma é um byte no ASCII. Cem emojis de rosto ocupam 400 bytes, porque cada um é um ponto de código de quatro bytes em UTF-8.
Glossário
Ponto de código: número que identifica um caractere no Unicode, escrito como U+ seguido de hexadecimal.
Repertório: o conjunto de caracteres que uma convenção reconhece, com um número para cada um.
Codificação: regra que transforma pontos de código em bytes, como UTF-8, UTF-16 ou Latin-1.
UTF-8: codificação do Unicode que usa de um a quatro bytes por ponto, e é compatível com ASCII.
UTF-16: codificação que usa unidades de 16 bits, com substitutos para pontos acima de U+FFFF.
Substitutos: par de unidades entre D800 e DFFF que juntas representam um ponto fora do plano básico.
Grafema: o que o leitor percebe como um caractere, podendo ser formado por vários pontos de código.
ZWJ: joiner de largura zero, U+200D, um caractere invisível que junta pontos de código num só grafema, como nas famílias de emoji.
Normalização: conversão de uma sequência de pontos para uma forma padrão, NFC (composta) ou NFD (decomposta).
Latin-1: codificação de um byte por caractere (ISO 8859-1), com 256 posições e sem o euro.
cp1252: página de código do Windows, parecida com Latin-1, mas que usa as posições de 128 a 159 para o euro e outros sinais.
Página de código: tabela que associa bytes a caracteres. Cada página reaproveita as mesmas posições para letras diferentes.
Runa: nome que o Go dá a um ponto de código.
Collation: regras de ordenação de um idioma, que dizem como comparar letras acentuadas e maiúsculas.
Endian: ordem dos bytes de uma unidade. Big endian grava primeiro o byte mais significativo, e little endian grava primeiro o menos significativo.
Mojibake: texto que aparece com caracteres errados porque foi lido com outra codificação.
BOM: marca de ordem de bytes, U+FEFF, usada no início de arquivos para indicar big ou little endian.
Fontes
Consultadas em 1 de novembro de 2026, salvo indicação.
- Verbete Baudot code, Wikipedia em inglês, para o código de cinco pulsos, a patente da máquina em 1874 (o código não foi patenteado), a adaptação de Murray para fita perfurada em 1901 e os modos de letras e figuras (ITA2). A data de 1870 para o início do trabalho de Baudot é de referências secundárias.
- Processo de Mimault contra Baudot em 1877, por resultados de busca que citam a disputa de prioridade e a decisão do tribunal civil de Paris, depois revista. Fonte primária não lida.
- Verbete ASCII, Wikipedia em inglês, para ASA X3.4-1963, as 28 posições sem atribuição e a revisão de 1967 (USAS X3.4-1967). E ascii-code.com/articles/ASCII-1963, pela afirmação de que a edição de 1963 não tinha minúsculas.
- Página de estatísticas do Unicode 17.0, unicode.org/versions/stats/charcountv17_0.html, para 159.629 gráficos, 172 de formato, 65 controles, 137.468 de uso privado, 2.048 substitutos, 66 não-caracteres, 814.664 reservados e 297.334 de Total Assigned.
- Anúncio do Unicode 17.0, blog.unicode.org, 9 de setembro de 2025, para a data e o acréscimo de 4.803 caracteres gráficos. Não foi reaberto nesta revisão.
- Relato de Rob Pike sobre a origem do UTF-8, transcrito por Markus Kuhn em cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt, para o jogo americano de papel, New Jersey e “setembro ou por aí de 1992”. E a busca de Russ Cox no arquivo, que data o primeiro rascunho em 2 de setembro de 1992. O original em doc.cat-v.org não abriu (HTTP 402 na rodada anterior).
- RFC 3629, UTF-8, a transformation format of ISO 10646, rfc-editor.org/info/rfc3629, publicada em novembro de 2003.
- Documentação de linguagem: especificação do Go para len e range sobre strings, o Java SE para String.length e codePointCount, a documentação do Python para str.encode e unicodedata, e o Intl.Collator e o Intl.Segmenter do Node. Os valores foram conferidos pelos programas, não por leitura de página.
O que não foi confirmado ou medido
Os números de bytes, pontos de código e tamanhos desta aula foram medidos pelos programas citados, com Go 1.27.2, OpenJDK 21.0.8, Python 3.12.8 e Node v24.21.0. A data de 1870 para o código Baudot vem de verbetes e histórias de referência. A afirmação de que a edição de 1963 do ASCII não tinha letras minúsculas vem de fontes secundárias; elas concordam nesse ponto, mas divergem em outros detalhes, e o documento X3.4-1963 não foi lido. O processo de Mimault e a data de 1877 vêm de resultados de busca, sem a sentença original. A data de 9 de setembro de 2025 para o Unicode 17.0 é de memória do anúncio, e o blog não foi reaberto nesta revisão. A regra de unidade de VARCHAR nos três bancos citados (MySQL, PostgreSQL e Oracle) foi escrita de memória, sem reabrir a documentação de cada um. A data do rascunho do UTF-8 em 2 de setembro de 1992 vem de uma busca no arquivo, e o relato de Pike fala só em setembro de 1992. Os nomes das tabelas de codificação de cada país não foram pesquisados um a um, e a aula trata apenas do caso de Latin-1 e cp1252. A tabela ASCII, com todas as 128 posições, não foi reproduzida. Nenhuma medida de desempenho de codificação ou decodificação foi feita nesta aula.
Relacionados
- Trilha de Hardware
- CPU
- Bits e binário
- Inteiros, complemento de dois e overflow
- Ponto flutuante
- Álgebra booleana e portas lógicas