Saltar al contenido
Topos Uranos

Resumen

Un std::string guarda bytes, no letras, y en el texto de hoy una letra puede ocupar de uno a cuatro bytes. Esta clase separa tres cosas que el habla cotidiana confunde: el carácter abstracto, que Unicode identifica con un número llamado punto de código; los bytes con que UTF-8 lo codifica, y la figura que la pantalla dibuja. Explica cómo UTF-8 reparte los bits de un punto de código entre sus bytes, por qué «año» ocupa cuatro y cómo se cuentan los caracteres de una cadena sin descodificarla. Muestra después qué operaciones rompen el texto (invertir byte a byte, pasar a mayúsculas byte a byte, cortar en medio de un carácter), cuáles funcionan sin cuidado alguno (guardar, copiar, concatenar, buscar, escribir) y qué no hace la biblioteca estándar de C++23: mayúsculas fuera de ASCII, orden alfabético según la lengua, normalización.

Objetivos de aprendizaje

  1. Explicar cómo UTF-8 codifica un punto de código en uno a cuatro bytes, y descodificar a mano los bytes de la «ñ».
  2. Predecir el tamaño en bytes de una cadena con letras no ASCII, como "año" (4) o "programación" (13).
  3. Contar los caracteres de una cadena UTF-8 contando los bytes que no son de continuación, con la condición (c & 0xC0) != 0x80.
  4. Identificar las operaciones que rompen el texto UTF-8 (invertir byte a byte, std::toupper por byte, cortar en medio de un carácter) y decidir qué hacer en cada caso.

Caracteres, puntos de código y bytes

Tres niveles

La clase Caracteres, booleanos, conversiones y constantes presentó el char como un entero de un byte que se escribe como letra según la tabla ASCII, y advirtió que 'ñ' no cabe en él; la clase Compilar, ejecutar y configurar mostró que "año" ocupa cuatro bytes y preparó la consola para escribirlo. Esta clase explica por qué. ASCII asigna un número a 128 caracteres, del 0 al 127, que caben en siete bits: las letras del inglés, las cifras, los signos de puntuación y algunos códigos de control. Ahora bien, el texto real contiene eñes, tildes, diéresis, letras griegas, ideogramas chinos, signos matemáticos y emojis, que suman cientos de miles. De ello se sigue que hace falta, primero, una tabla mucho mayor, y segundo, una manera de escribir sus números en bytes.

Conviene distinguir tres niveles. El primero es el carácter abstracto: la letra «eñe minúscula», con independencia de cómo se guarde o se dibuje. Unicode es la norma que asigna a cada carácter abstracto un número, su punto de código, que se escribe U+ seguido de al menos cuatro cifras hexadecimales: la «ñ» es U+00F1 (241), la «A» es U+0041 (65, el mismo número que en ASCII), el «€» es U+20AC (8364). Los puntos de código van de 0 a U+10FFFF, poco más de un millón. El segundo nivel es la codificación: la regla que convierte cada punto de código en una sucesión de bytes. UTF-8 es la codificación que usan la web, los sistemas actuales y este curso. El tercer nivel es la presentación: la figura que la fuente tipográfica dibuja en la pantalla, que puede cambiar de un tipo de letra a otro sin que cambie el carácter.

Si esto es así, un dato textual no es un texto mientras no se sepa su codificación: los bytes c3 b1 son la «ñ» en UTF-8, pero leídos con la tabla de una consola antigua son dos símbolos sin relación con ella, que es el fallo que la clase 1.4 enseñó a diagnosticar. La misma cuestión, vista desde la composición de documentos, aparece en la clase «Idiomas y tipografías» del curso LaTeX para la Redacción Académica.

Ver los bytes

Para ver los bytes de una cadena basta recorrerla: un std::string y un std::string_view son secuencias de char, y el bucle for por rango entrega uno por uno sus bytes. Para escribirlos en hexadecimal sirve el campo {:02x} (dos cifras hexadecimales, con cero a la izquierda). Como en la clase 2.4 con {:d}, C++23 escribe con él un char como byte sin signo. La regla importa, porque char tiene signo en GCC sobre x86-64: el byte c3, convertido en int, es el número −61-61, que {:02x} escribiría -3d.

Programa en C++
#include <print>#include <string_view>​// Escribe text, su tamaño en bytes y cada byte en 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");}
Salida
A   1: 41ñ   2: c3 b1€   3: e2 82 ac😀  4: f0 9f 98 80año 4: 61 c3 b1 6f

Cada línea muestra el texto, su tamaño y sus bytes. Un carácter de cada longitud: la «A» ocupa un byte, la «ñ» dos, el «€» tres y el emoji cuatro. La palabra «año» ocupa cuatro bytes, 61, c3 b1 y 6f, y su size() es 4, no 3. Obsérvese que la «A» es el byte 41, el mismo que en ASCII: UTF-8 escribe los 128 caracteres de ASCII exactamente como ASCII.

EjemploDe la eñe a sus dos bytes

La figura sigue la codificación de la «ñ» de arriba abajo: el punto de código, sus bits, el reparto de esos bits y los dos bytes resultantes, con los bits de prefijo destacados.

Demostración

  1. La «ñ» es el punto de código U+00F1, es decir, 15⋅16+1=24115 \cdot 16 + 1 = 241.

  2. En binario, 241=128+64+32+16+1241 = 128 + 64 + 32 + 16 + 1 se escribe 11110001. Es mayor que 127 y menor que 2048, de modo que UTF-8 lo escribe en dos bytes, que transportan once bits: se completa con ceros a la izquierda hasta once, 00011110001.

  3. Los once bits se reparten en dos grupos: los cinco primeros, 00011, y los seis últimos, 110001.

  4. El primer byte lleva el prefijo 110, que anuncia un carácter de dos bytes, seguido de los cinco bits del primer grupo: 11000011. El segundo lleva el prefijo 10, que marca un byte de continuación, seguido de los seis bits restantes: 10110001.

  5. En hexadecimal, 1100 0011 es c3 y 1011 0001 es b1: los dos bytes que el programa anterior escribió para la «ñ». El cambio de base entre binario y hexadecimal, de cuatro en cuatro bits, es el de la clase «Sistemas de numeración posicional» de Álgebra y Geometría I.