Кодировка Unicode и UTF-8: как кодируются символы
Unicode - это единая таблица, в которой у каждого символа любого языка мира есть свой уникальный номер, кодовая точка. Но сама по себе кодовая точка - это просто число, а компьютер хранит и передаёт только байты. UTF-8 - это правило, по которому число превращается в последовательность байт: одну для латиницы, две-три для кириллицы и большинства алфавитов, четыре для эмодзи и редких иероглифов. Ниже разберём, откуда берётся кодовая точка, как устроены байтовые префиксы UTF-8 и почему разработчики выбрали именно такую схему, а не что-то проще. Чтобы сразу увидеть, как конкретный символ превращается в байты, покрути калькулятор ниже: он раскладывает кодовую точку на биты и собирает из них байты UTF-8 на глазах.
Зачем нужен Unicode
До Unicode у каждой кодировки была своя таблица символов: ASCII знала только латиницу и цифры, кодировки вроде Windows-1251 добавляли кириллицу за счёт вытеснения других символов, а японские и китайские тексты вообще жили в отдельных многобайтовых стандартах. Один и тот же байт в разных кодировках означал разные буквы, поэтому текст, сохранённый в одной кодировке и открытый в другой, превращался в "кракозябры". Unicode решает эту проблему радикально: он присваивает уникальный номер, кодовую точку, каждому символу каждого письменного языка, знаку препинания, эмодзи и даже управляющему символу - независимо от платформы, программы и языка. Сам Unicode описывает только соответствие "символ - номер"; как этот номер хранить в байтах, определяют отдельные схемы кодирования, и самая распространённая из них в интернете - UTF-8.
Кодовая точка и запись U+XXXX
Кодовую точку принято записывать в виде U+XXXX, где XXXX - номер символа в шестнадцатеричной системе. Буква A имеет кодовую точку U+0041 (десятичное 65 - то же значение, что и в ASCII), русская буква "я" - U+044F (1103), знак евро "€" - U+20AC (8364), иероглиф "中" - U+4E2D (20013), а эмодзи "😀" - U+1F600 (128512). Диапазон Unicode ограничен значением U+10FFFF, то есть 1 114 111 в десятичной записи - это исторический предел, о котором поговорим отдельно ниже. Именно кодовую точку, а не сам символ, обрабатывают алгоритмы кодирования: чтобы получить байты UTF-8, нужно взять двоичную запись этого числа и правильно разложить её биты по байтам.
Как устроена UTF-8: префиксы и продолжающие байты
UTF-8 - кодировка переменной длины: короткие, часто встречающиеся коды занимают меньше байт, редкие - больше. Правило простое и завязано на величину кодовой точки :
- если (U+007F) - один байт вида 0xxxxxxx, где семь x - это сама кодовая точка в двоичном виде;
- если (U+07FF) - два байта: первый 110xxxxx несёт пять старших бит, второй 10xxxxxx - шесть младших;
- если (U+FFFF) - три байта: 1110xxxx, 10xxxxxx, 10xxxxxx, вместе они несут 16 значащих бит;
- если (U+10FFFF) - четыре байта: 11110xxx, 10xxxxxx, 10xxxxxx, 10xxxxxx, вместе 21 бит.
Первый байт всегда сообщает длину всей последовательности: по числу единиц перед первым нулём в его старшем полубайте сразу видно, сколько байт займёт символ - один, два, три или четыре. Все байты, кроме первого, начинаются с фиксированного префикса 10xxxxxx - это позволяет декодеру, открыв файл с любого места, мгновенно понять, что перед ним "хвост" многобайтового символа, а не начало нового. Такая избыточность - цена, которую UTF-8 платит за самосинхронизацию: даже если байты потеряны или файл открыт не с начала, декодер быстро находит границу следующего символа.
Разбор примера: символ € в UTF-8
Возьмём знак евро, кодовая точка U+20AC, то есть 8364 в десятичной записи. Двоичная запись этого числа - 10000010101100, ровно 14 значащих бит, и по правилу выше оно попадает в диапазон трёх байт (до 65535). Разбиваем 16 доступных бит на группы 4+6+6, дополняя старшую группу нулями: 0010 000010 101100. Теперь собираем байты:
Итоговая последовательность - E2 82 AC в шестнадцатеричном виде, три байта. Именно такую тройку байт вы увидите, если открыть файл с этим символом в hex-редакторе. Обратный процесс, декодирование, работает зеркально: декодер видит первый байт с префиксом 1110, узнаёт по нему, что символ занимает три байта, забирает четыре бита из первого байта и по шесть из каждого продолжающего, склеивает их в число 8364 и смотрит это значение в таблице Unicode.

Тот же принцип работает и для эмодзи, только байт становится четыре: кодовая точка "😀" (U+1F600, 128512) лежит за пределами Basic Multilingual Plane - базовой плоскости, где помещаются почти все буквенные алфавиты, - поэтому попадает в диапазон четырёх байт с 21 значащим битом.
UTF-8 против UTF-16 и UTF-32
У Unicode есть и другие схемы кодирования байтов, и они не всегда дают тот же результат, что UTF-8. UTF-32 - самая простая по устройству: каждый символ занимает ровно четыре байта, без исключений. Это удобно для вычислений (номер символа в строке всегда можно найти прямым умножением на 4), но крайне расточительно для обычного текста на латинице или кириллице. UTF-16 - компромисс: символы из базовой плоскости (до U+FFFF, а это буквы почти всех живых языков) занимают два байта, а более редкие символы вроде эмодзи и старинных письменностей кодируются суррогатной парой - двумя двухбайтовыми "половинками", то есть тоже четырьмя байтами.
Из-за этого разные кодировки по-разному "весят" один и тот же текст. Латинское слово в UTF-8 займёт по одному байту на букву, а в UTF-16 или UTF-32 - вдвое-вчетверо больше. А вот слово "Привет" в UTF-8 займёт по два байта на букву (12 байт на шесть букв), и ровно столько же в UTF-16 - для кириллицы разница между этими двумя кодировками стирается, потому что все её буквы лежат в базовой плоскости. Для иероглифов и знака евро (три байта в UTF-8) UTF-16 иногда оказывается компактнее (два байта), поэтому веб отдаёт предпочтение UTF-8 в основном из-за полной обратной совместимости с ASCII и удобства работы с текстовыми протоколами, а не из-за абсолютной экономии байт на каждом символе.
Почему максимум - U+10FFFF
Кодовая точка Unicode искусственно ограничена значением U+10FFFF, хотя изначальная версия UTF-8 (1993 года) умела кодировать числа вплоть до 0x7FFFFFFF пятью и шестью байтами. Ограничение появилось из-за UTF-16: суррогатные пары строятся из двух 16-битных половинок, и арифметика этого механизма физически не может адресовать больше 2^20 дополнительных кодовых точек сверх базовой плоскости, что и даёт верхнюю границу U+10FFFF. Дополнительное следствие того же механизма - диапазон U+D800–U+DFFF (55 296–57 343) зарезервирован под сами половинки суррогатных пар и не может использоваться как кодовая точка настоящего символа: если такое значение попадает в декодер UTF-8, это ошибка данных. В 2003 году стандарт RFC 3629 официально урезал UTF-8 до четырёх байт и того же предела U+10FFFF, синхронизировав его с UTF-16 - так все схемы кодирования Unicode стали описывать один и тот же набор символов.
Частые ошибки
- Путать символ и байт. "Длина строки" в символах и её размер в байтах - разные числа, если в строке есть кириллица, эмодзи или иероглифы: один символ может занимать от 1 до 4 байт.
- Забывать про суррогатный диапазон. Значения U+D800–U+DFFF нельзя использовать как самостоятельную кодовую точку - они существуют только парами внутри UTF-16.
- Путать длину группы бит с длиной байта. В байтах-продолжениях 10xxxxxx полезны только шесть младших бит, а не восемь - первые два бита это служебный префикс.
- Проверять число байт по символу, а не по кодовой точке. Решает не то, "буква это или иероглиф", а числовое значение кодовой точки относительно границ 127, 2047 и 65535.
- Путать UTF-8 с самим Unicode. Unicode - таблица номеров символов, UTF-8 - лишь один из способов записать эти номера байтами; есть ещё UTF-16 и UTF-32.
FAQ
Сколько байт занимает один символ в UTF-8? От одного до четырёх, в зависимости от величины кодовой точки: латиница и цифры - один байт, кириллица, греческий, знаки вроде € - два-три байта, эмодзи и редкие иероглифы - четыре байта.
Почему в UTF-8 первый байт начинается с разного числа единиц? Число единиц перед первым нулём в первом байте (110, 1110 или 11110) сразу сообщает декодеру, сколько байт занимает символ целиком - это позволяет читать текст, не заглядывая вперёд.
Чем UTF-8 лучше UTF-16? Для текста на латинице UTF-8 компактнее и полностью совместим с ASCII побайтово, поэтому это основная кодировка веба; для текста, где преобладают иероглифы, UTF-16 иногда даёт меньший размер файла.
Коротко
Unicode присваивает уникальный номер, кодовую точку U+XXXX, каждому символу любого языка, а UTF-8 превращает этот номер в один, два, три или четыре байта по строгим правилам с префиксами 0, 110/10, 1110/10 и 11110/10. Число байт растёт вместе с величиной кодовой точки - латиница укладывается в один байт, кириллица и большинство алфавитов в два-три, редкие символы и эмодзи в четыре. Верхняя граница U+10FFFF и запрет на диапазон U+D800–U+DFFF - не случайность, а следствие совместимости с UTF-16 через механизм суррогатных пар.
Читайте также

Подсчёт символов в строке: длина, пробелы, байты
Подсчёт символов в строке простыми словами: чем длина с пробелами отличается от длины без них, почему len со счётом эмодзи и кириллицы расходится, как считать символы на Python и JavaScript.

Атрибуты сущности в ER-модели: пять типов и примеры
Разбираем пять типов атрибутов сущности в ER-модели: простой, составной, ключевой, многозначный, производный, с примерами и переходом к столбцам и таблицам реляционной схемы.

Циркуляция векторного поля по контуру: формула и смысл
Циркуляция векторного поля по контуру: что это такое, как вычислить линейный интеграл по параметризации и через ротор поля с теоремой Грина, разбор типичных ошибок и примеров расчёта.

Динамическое программирование: основы и идея мемоизации
Что такое динамическое программирование простыми словами: перекрывающиеся подзадачи, оптимальная подструктура, мемоизация и табуляция на примере чисел Фибоначчи, разница с наивной рекурсией.

Начальные и центральные моменты случайной величины: формулы
Разбираем начальные и центральные моменты случайной величины: как выразить дисперсию, асимметрию и эксцесс через ν_k и вычислить их на примере дискретного распределения.

Натуральная величина сечения многогранника плоскостью
Как найти натуральную величину сечения многогранника плоскостью: способы замены плоскостей проекций и вращения, формула через угол наклона, площадь и периметр сечения на примере задачи.