Saltar para o conteúdo
Topos Uranos

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

  1. Explicar como o UTF-8 codifica um ponto de código em um a quatro bytes, e decodificar à mão os bytes do “ñ”.
  2. Prever o tamanho em bytes de uma string com letras não ASCII, como "año" (4) ou "programación" (13).
  3. 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.
  4. Identificar as operações que quebram o texto UTF-8 (inverter byte a byte, std::toupper por 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 −61-61, que {:02x} escreveria -3d.

Programa em C++
#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");}
Saída
A   1: 41ñ   2: c3 b1€   3: e2 82 ac😀  4: f0 9f 98 80año 4: 61 c3 b1 6f

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

ExemploDo ñ a seus dois bytes

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

  1. O “ñ” é o ponto de código U+00F1, isto é, 15⋅16+1=24115 \cdot 16 + 1 = 241.

  2. Em binário, 241=128+64+32+16+1241 = 128 + 64 + 32 + 16 + 1 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.

  3. Os onze bits se repartem em dois grupos: os cinco primeiros, 00011, e os seis últimos, 110001.

  4. 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 prefixo 10, que marca um byte de continuação, seguido dos seis bits restantes: 10110001.

  5. Em hexadecimal, 1100 0011 é c3 e 1011 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.