Краткое содержание
Строка std::string хранит байты, а не буквы, и в современном тексте одна буква может занимать от одного до четырёх байтов. Этот урок разделяет три вещи, которые обыденная речь смешивает: абстрактный символ, который Юникод обозначает числом, называемым кодовой точкой; байты, которыми его кодирует UTF-8; и изображение, которое рисует экран. Урок объясняет, как UTF-8 распределяет биты кодовой точки по её байтам, почему «дом» занимает шесть байтов и как сосчитать символы строки, не декодируя её. Затем он показывает, какие операции портят текст (обращение порядка байт за байтом, перевод в прописные байт за байтом, разрез посреди символа), какие работают без всяких предосторожностей (хранение, копирование, конкатенация, поиск, вывод) и чего не делает стандартная библиотека C++23: прописных букв за пределами ASCII, алфавитного порядка по правилам языка, нормализации.
Цели обучения
- Объяснить, как UTF-8 кодирует кодовую точку байтами числом от одного до четырёх, и вручную декодировать байты испанской буквы «ñ».
- Предсказывать размер в байтах строки с буквами вне ASCII, например
"дом"(6) или"программа"(18). - Считать символы строки UTF-8, подсчитывая байты, которые не являются байтами продолжения, с помощью условия
(c & 0xC0) != 0x80. - Распознавать операции, которые портят текст 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, есть число , которое {:02x} вывело бы как -3d.
#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.
Рисунок прослеживает кодирование «ñ» сверху вниз: кодовую точку, её биты, распределение этих битов и два получившихся байта, с выделенными битами префиксов.
Доказательство
Кодовая точка «ñ» равна
U+00F1, то есть .В двоичной системе записывается как
11110001. Это число больше 127 и меньше 2048, поэтому UTF-8 записывает его двумя байтами, которые несут одиннадцать битов: запись дополняется нулями слева до одиннадцати знаков,00011110001.Одиннадцать битов делятся на две группы: первые пять,
00011, и последние шесть,110001.Первый байт несёт префикс
110, который объявляет символ из двух байтов, а за ним пять битов первой группы:11000011. Второй несёт префикс10, который отмечает байт продолжения, а за ним шесть оставшихся битов:10110001.В шестнадцатеричной системе
1100 0011записывается какc3, а1011 0001какb1: это те два байта, которые предыдущая программа вывела для «ñ». Переход между двоичной и шестнадцатеричной системами, по четыре бита, рассмотрен в уроке «Позиционные системы счисления» курса «Алгебра и геометрия I».
Загрузка содержимого…