Перейти к содержимому
Topos Uranos

Краткое содержание

Строка std::string хранит байты, а не буквы, и в современном тексте одна буква может занимать от одного до четырёх байтов. Этот урок разделяет три вещи, которые обыденная речь смешивает: абстрактный символ, который Юникод обозначает числом, называемым кодовой точкой; байты, которыми его кодирует UTF-8; и изображение, которое рисует экран. Урок объясняет, как UTF-8 распределяет биты кодовой точки по её байтам, почему «дом» занимает шесть байтов и как сосчитать символы строки, не декодируя её. Затем он показывает, какие операции портят текст (обращение порядка байт за байтом, перевод в прописные байт за байтом, разрез посреди символа), какие работают без всяких предосторожностей (хранение, копирование, конкатенация, поиск, вывод) и чего не делает стандартная библиотека C++23: прописных букв за пределами ASCII, алфавитного порядка по правилам языка, нормализации.

Цели обучения

  1. Объяснить, как UTF-8 кодирует кодовую точку байтами числом от одного до четырёх, и вручную декодировать байты испанской буквы «ñ».
  2. Предсказывать размер в байтах строки с буквами вне ASCII, например "дом" (6) или "программа" (18).
  3. Считать символы строки UTF-8, подсчитывая байты, которые не являются байтами продолжения, с помощью условия (c & 0xC0) != 0x80.
  4. Распознавать операции, которые портят текст UTF-8 (обращение байт за байтом, std::toupper для каждого байта, разрез посреди символа), и решать, что делать в каждом случае.

Символы, кодовые точки и байты

Три уровня

Урок Символы, логические значения, преобразования и константы представил char как целое число размером в один байт, которое выводится как буква по таблице ASCII, и предупредил, что 'б' в нём не помещается; урок Компиляция, запуск и настройка показал, что "год" занимает шесть байтов, и подготовил консоль к его выводу. Этот урок объясняет почему. ASCII сопоставляет номера 128 символам, от 0 до 127, которые умещаются в семь битов: буквам английского алфавита, цифрам, знакам препинания и нескольким управляющим кодам. Однако реальный текст содержит кириллицу, буквы с диакритическими знаками, греческие буквы, китайские иероглифы, математические знаки и эмодзи, а их сотни тысяч. Отсюда следует, что нужна, во-первых, гораздо большая таблица и, во-вторых, способ записывать её номера байтами.

Полезно различать три уровня. Первый уровень есть абстрактный символ: буква «строчная эн с тильдой» испанского алфавита, независимо от того, как она хранится или рисуется. Юникод является стандартом, который сопоставляет каждому абстрактному символу число, его кодовую точку, записываемую как U+ и не менее четырёх шестнадцатеричных цифр: кодовая точка «ñ» равна U+00F1 (241), кодовая точка «A» равна U+0041 (65, то же число, что в ASCII), кодовая точка «€» равна U+20AC (8364). Кодовые точки идут от 0 до U+10FFFF, и их немногим больше миллиона. Второй уровень есть кодировка: правило, которое превращает каждую кодовую точку в последовательность байтов. UTF-8 является кодировкой, которую используют веб, современные системы и этот курс. Третий уровень есть отображение: изображение, которое шрифт рисует на экране и которое может меняться от одного шрифта к другому, хотя символ остаётся тем же.

Если это так, то текстовые данные не являются текстом, пока неизвестна их кодировка: байты c3 b1 в UTF-8 означают букву «ñ», но, прочитанные по таблице старой консоли, они дают два знака, не имеющих к ней никакого отношения; именно такой сбой урок 1.4 учил распознавать. Тот же вопрос, рассмотренный со стороны вёрстки документов, возникает в уроке «Языки и шрифты» курса «LaTeX для академического письма».

Как увидеть байты

Чтобы увидеть байты строки, достаточно её обойти: std::string и std::string_view являются последовательностями char, и цикл for по диапазону выдаёт их байты один за другим. Для вывода в шестнадцатеричном виде служит поле {:02x} (две шестнадцатеричные цифры, с нулём слева). Как и {:d} в уроке 2.4, и в силу той же поправки к стандарту, принятой в 2023 году и реализованной в GCC 15, {:02x} выводит char как байт без знака. Это правило важно, потому что в GCC на x86-64 тип char знаковый: байт c3, преобразованный в int, есть число −61-61, которое {:02x} вывело бы как -3d.

Программа на C++
#include <print>#include <string_view>​// Байты текста.// Предусловие: нет.// Постусловие: выводит строку с text, его размером в байтах и каждым байтом в шестнадцатеричном виде.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   1: 41ñ   2: c3 b1€   3: e2 82 ac😀  4: f0 9f 98 80дом 6: d0 b4 d0 be d0 bc

Каждая строка вывода показывает текст, его размер и его байты. По одному символу каждой длины: «A» занимает один байт, «ñ» два, «€» три, а эмодзи четыре. Слово «дом» занимает шесть байтов, d0 b4, d0 be и d0 bc, и его size() равен 6, а не 3. Заметим, что «A» записывается байтом 41, тем же, что в ASCII: UTF-8 записывает 128 символов ASCII в точности как ASCII.

ПримерОт «ñ» к её двум байтам

Рисунок прослеживает кодирование «ñ» сверху вниз: кодовую точку, её биты, распределение этих битов и два получившихся байта, с выделенными битами префиксов.

Доказательство

  1. Кодовая точка «ñ» равна U+00F1, то есть 15⋅16+1=24115 \cdot 16 + 1 = 241.

  2. В двоичной системе 241=128+64+32+16+1241 = 128 + 64 + 32 + 16 + 1 записывается как 11110001. Это число больше 127 и меньше 2048, поэтому UTF-8 записывает его двумя байтами, которые несут одиннадцать битов: запись дополняется нулями слева до одиннадцати знаков, 00011110001.

  3. Одиннадцать битов делятся на две группы: первые пять, 00011, и последние шесть, 110001.

  4. Первый байт несёт префикс 110, который объявляет символ из двух байтов, а за ним пять битов первой группы: 11000011. Второй несёт префикс 10, который отмечает байт продолжения, а за ним шесть оставшихся битов: 10110001.

  5. В шестнадцатеричной системе 1100 0011 записывается как c3, а 1011 0001 как b1: это те два байта, которые предыдущая программа вывела для «ñ». Переход между двоичной и шестнадцатеричной системами, по четыре бита, рассмотрен в уроке «Позиционные системы счисления» курса «Алгебра и геометрия I».