Resumo
Uma std::string guarda bytes, não letras, e no texto de hoje uma letra pode ocupar de um a quatro bytes. Esta aula separa três coisas que a fala cotidiana confunde: o caractere abstrato, que o Unicode identifica com um número chamado ponto de código; os bytes com que o UTF-8 o codifica; e a figura que a tela desenha. Explica como o UTF-8 reparte os bits de um ponto de código entre seus bytes, por que a palavra espanhola “año” (ano) ocupa quatro e como se contam os caracteres de uma string sem decodificá-la. Mostra depois que operações quebram o texto (inverter byte a byte, passar para maiúsculas byte a byte, cortar no meio de um caractere), quais funcionam sem cuidado algum (guardar, copiar, concatenar, buscar, escrever) e o que a biblioteca padrão do C++23 não faz: maiúsculas fora do ASCII, ordem alfabética segundo a língua, normalização.
Objetivos de aprendizagem
- Explicar como o UTF-8 codifica um ponto de código em um a quatro bytes, e decodificar à mão os bytes do “ñ”.
- Prever o tamanho em bytes de uma string com letras não ASCII, como
"año"(4) ou"programación"(13). - Contar os caracteres de uma string UTF-8 contando os bytes que não são de continuação, com a condição
(c & 0xC0) != 0x80. - Identificar as operações que quebram o texto UTF-8 (inverter byte a byte,
std::toupperpor byte, cortar no meio de um caractere) e decidir o que fazer em cada caso.
Caracteres, pontos de código e bytes
Três níveis
A aula Caracteres, booleanos, conversões e constantes apresentou o char como um inteiro de um byte que se escreve como letra segundo a tabela ASCII, e advertiu que 'ñ' não cabe nele; a aula Compilar, executar e configurar mostrou que "año" ocupa quatro bytes e preparou o console para escrevê-lo. Esta aula explica por quê. O ASCII atribui um número a 128 caracteres, do 0 ao 127, que cabem em sete bits: as letras do inglês, os algarismos, os sinais de pontuação e alguns códigos de controle. Ora, o texto real contém o ñ, acentos, tremas, letras gregas, ideogramas chineses, sinais matemáticos e emojis, que somam centenas de milhares. Daí se segue que é preciso, primeiro, uma tabela muito maior, e segundo, uma maneira de escrever seus números em bytes.
Convém distinguir três níveis. O primeiro é o caractere abstrato: a letra “ñ minúsculo”, independentemente de como se guarde ou se desenhe. O Unicode é o padrão que atribui a cada caractere abstrato um número, seu ponto de código, que se escreve U+ seguido de pelo menos quatro algarismos hexadecimais: o “ñ” é U+00F1 (241), o “A” é U+0041 (65, o mesmo número que no ASCII), o “€” é U+20AC (8364). Os pontos de código vão de 0 a U+10FFFF, pouco mais de um milhão. O segundo nível é a codificação: a regra que converte cada ponto de código em uma sequência de bytes. O UTF-8 é a codificação que usam a web, os sistemas atuais e este curso. O terceiro nível é a apresentação: a figura que a fonte tipográfica desenha na tela, que pode mudar de um tipo de letra para outro sem que mude o caractere.
Se isso é assim, um dado textual não é um texto enquanto não se souber sua codificação: os bytes c3 b1 são o “ñ” em UTF-8, mas lidos com a tabela de um console antigo são dois símbolos sem relação com ele, que é a falha que a aula 1.4 ensinou a diagnosticar. A mesma questão, vista a partir da composição de documentos, aparece na aula “Idiomas e tipografias” do curso LaTeX para a Redação Acadêmica.
Ver os bytes
Para ver os bytes de uma string basta percorrê-la: uma std::string e uma std::string_view são sequências de char, e o laço for baseado em intervalo entrega um a um seus bytes. Para escrevê-los em hexadecimal serve a especificação de formato {:02x} (dois algarismos hexadecimais, com zero à esquerda). Como com {:d} na aula 2.4, e pela mesma correção do padrão adotada em 2023 e aplicada pelo GCC 15, {:02x} escreve um char como byte sem sinal. A regra importa, porque char tem sinal no GCC sobre x86-64: o byte c3, convertido em int, é o número , que {:02x} escreveria -3d.
#include <print>#include <string_view>// Bytes de um texto.// Pré-condição: nenhuma.// Pós-condição: escreve uma linha com text, seu tamanho em bytes e cada byte em hexadecimal.void printBytes(std::string_view text){ std::print("{:<4}{}:", text, text.size()); for (const char c : text) { std::print(" {:02x}", c); } std::println("");}int main(){ printBytes("A"); printBytes("ñ"); printBytes("€"); printBytes("😀"); printBytes("año");}A 1: 41ñ 2: c3 b1€ 3: e2 82 ac😀 4: f0 9f 98 80año 4: 61 c3 b1 6fCada linha mostra o texto, seu tamanho e seus bytes. Um caractere de cada comprimento: o “A” ocupa um byte, o “ñ” dois, o “€” três e o emoji quatro. A palavra “año” ocupa quatro bytes, 61, c3 b1 e 6f, e seu size() é 4, não 3. Observe-se que o “A” é o byte 41, o mesmo que no ASCII: o UTF-8 escreve os 128 caracteres do ASCII exatamente como o ASCII.
A figura acompanha a codificação do “ñ” de cima para baixo: o ponto de código, seus bits, a repartição desses bits e os dois bytes resultantes, com os bits de prefixo destacados.
Demonstração
O “ñ” é o ponto de código
U+00F1, isto é, .Em binário, se escreve
11110001. É maior que 127 e menor que 2048, de modo que o UTF-8 o escreve em dois bytes, que transportam onze bits: completa-se com zeros à esquerda até onze,00011110001.Os onze bits se repartem em dois grupos: os cinco primeiros,
00011, e os seis últimos,110001.O primeiro byte leva o prefixo
110, que anuncia um caractere de dois bytes, seguido dos cinco bits do primeiro grupo:11000011. O segundo leva o prefixo10, que marca um byte de continuação, seguido dos seis bits restantes:10110001.Em hexadecimal,
1100 0011éc3e1011 0001éb1: os dois bytes que o programa anterior escreveu para o “ñ”. A mudança de base entre binário e hexadecimal, de quatro em quatro bits, é a da aula “Sistemas de numeração posicional” de Álgebra e Geometria I.
Carregando o conteúdo…