Подавляющее число примеров кодов со строками C/C++ (в любых публикуемых источниках) оперирует с нуль-терминальными массивами (ASCIZ) элементов char (в стиле C), или с контейнерным типом string (в стиле C++), построенным как надстройка над такими массивами.
Всё это замечательно работает со строками латинских (англоязычных) символов, но может идти вразнос на строках, содержащих символы иноязычных алфавитов (русский, китайский, арабский или иврит). Здесь всё не так просто… и весьма плохо описано в литературе, что и понятно: англоязычных авторов мало занимают вопросы иноязычной локализации, а отечественные авторы, в большинстве, переписывающие и адаптирующие англоязычные публикации, не уделяют внимания этой стороне вопроса.
Язык C — весьма старый язык программирования, а C++ наследует из него форматы и сдерживается требованиями синтаксической совместимости с C. Для того, чтобы не иметь в C/C++ проблем с такими строками (называемыми локализованными) нужно понимать что там с этими локализациями происходит…
Исторически, символы (char) представлялись (1963 год) стандартом ASCII как младшие 7 бит одного байта, при этом старший 8-й бит предназначался для контроля ошибок, возникших при передаче данных.
Такая кодировка позволяет кодировать максимально всего 128 различных символов, и этого числа с трудом хватает на символы английского алфавита (большие и малые), цифровые (коды 0x30-0x39), управляющие (код меньше 0x20) и специальные символы. Когда возникала необходимость представления национальных алфавитов, вводилась альтернативная таблица символов, например KOI-7 для русского языка.
Переключение в потоке ввода-вывода на альтернативную таблицу символов производилось символом с кодом 0x18 (код называется: Device Control 2) в потоке, а возврат к основную таблицу ASCII — символом с кодом 0x17 (Device Control 1).
Позже, с середины 80-х годов, с времени широкого распространения IBM PC и замены ними компьютеров других семейств, стандарт ASCII был расширен на 8-й бит байта char, байт мог представлять 256 символов: младшие 127 представляли исходную таблицу ASCII (с латинским шрифтом), а старшие — национальный алфавит.
Но, поскольку национальные алфавиты могут быть самыми разнообразными, то для поддержки каждого из них потребовалось ввести свою кодовую страницу, например, для русского языка это могут быть страницы CP-866 (в MS-DOS), CP-1251 (в Windows), KOI-8r (в UNIX, Linux) — и каждая из этих страниц предлагает свой, отличающийся от других, порядок символов русского языка. При этом, для корректного отображения (или декодирования) любой локализованной символьной строки нужно обязательно знать кодовую страницу в которой она представлена.
Для того, чтобы положить конец этому вавилонскому столпотворению языковых кодовых страниц, был предложен (1991г.) стандарт представления UNICODE, в системе кодирования которого каждый символ кодируется 32-бит значением (4 байта, но не все 32-бит значения допустимы). Применение данного стандарта позволяет закодировать огромное количество символов разных систем письменности.
Документы, закодированные по стандарту UNICODE, могут содержать в едином тексте японские и китайские иероглифы, буквы латиницы, кириллицы, греческого алфавита (α, ε, θ, π, σ, λ, φ, Ω…), математические символы, символы музыкальной нотной нотации, символы вымерших, редких, экзотических народностей. При этом нет необходимости в переключении кодовых страниц. Например, вот как выглядят некоторые символы языка, обозначенного как “сингальский”:
1 | ඣ, ඤ, ඥ |
Первый UNICODE стандарт был выпущен в 91-м году. Последний на данный момент — в 2017 и он описывает 136755 разнообразных символов.
Но UNICODE — это ещё только стандарт представления каждого символа. Для представления этого символа в конкретной операционной системе (или языке программирования) нужна ещё система кодирования символов UNICODE.
- Достаточно широко используются системы кодирования:
UTF-8 — для представления каждого символа используются 4 байта, непосредственное численное значение кода UNICODE - UTF-16 — для представления наиболее часто используемых символов используются 2 байта (первые 65536 позиций), а остальные представляются в виде в виде «суррогатных пар». Такое кодирование используется в операционных системах Windows начиная с Windows NT.
- UTF-32 — для представления каждого символа используются последовательность байт переменной длины: от 1 байта для символов основной таблицы ASCII, до 6 байт для редко используемых символов (символы русского алфавита кодируются 2-мя байтами). Эта кодировка создавалась позже других для операционных систем Plan 9 и Inferno в 1992г. Кеном Томпсоном и Робертом Пайком с коллегами, и вошла как единая и основная кодировка символьных строк в более поздних языках программирования Python и Go. Такое кодирование используется, на сегодня повсеместно, в POSIX/UNIX операционных системах, Linux.
Возвращаясь к тому, что C/C++ старое семейство языков программирования, для представления в них локализованных символов потребовалось ввести новый тип данных — широкие символы wchar_t вместо char (тип данных появился в стандарте C89, но, в полной мере с API поддержки, только в стандарте C99). Вместо строчных функций библиотеки C вида str*() для широких предлагаются их полные аналоги, но в виде wcs*() (вместо префикса str записываем префикс wcs). В разных системах wchar_t может иметь разную разрядность (в Linux это int32_t, в Windows int16_t) но для программиста это не имеет значения и не создаёт различий.
1 2 3 4 5 6 | #include <stdio.h> #include <wchar.h> int main( void ) { printf( "размер символа wchar_t вашей реализации = %d байт\n", (int)sizeof( wchar_t ) ); } |
1 2 | $ ./0 размер символа wchar_t вашей реализации = 4 байт |
Для работы и преобразования многобайтовых последовательностей записанных в кодировке UTF-8 в C/C++ вводится семейство функций вида mb*(): mbtowc(), mblen(), mbstowcs(), wcstombs() и др. Это механизм взаимных преобразований между массивами char[] (в которых также выражаются строки UTF-8) и wchar_t[]. Если вы не сталкиваетесь с кодировкой UTF-8 (что с большой вероятностью имеет место в Windows), то эта группа функций вас не должна занимать.
Аналогично, вместо контейнерного класса C++ string вводится аналогичный контейнерный класс широких символов wstring.
Конкретно о технике работы с широкими локализованными строками мы поговорим в следующей статье. А пока 1-й элементарный пример … без комментариев — как повод для размышления (обратите внимание и объясните, что вызов strlen() в каждом случае даёт число байт в строке явно не соответствующее визуально видимое число букв в ней):
1 2 3 4 5 6 | #include <stdio.h> #include <string.h> int main() { char str[] = "Привет, 世界"; printf( "%s [%d байт]\n", str, (int)strlen( str ) ); } |
1 2 | $ ./1 Привет, 世界 [20 байт] |
P.S. С большой детальностью про локализацию в C/C++ и работе с локализованными строками, кто интересуется с большей подробностью, могут почитать здесь: Языковая локализация C/C++ Языковая локализация C/C++ — там объяснений более 22 страниц формата офисного документа.
Некоторые особенности работы с русскоязычными строками, которые не описываются обычно при рассмотрении строк char, будут рассмотрены в следующих статьях в продолжение этой темы.
Следите…
Я так понимаю в текст вкралась опечатка, и речь идет о последовательности UTF-8, 16 и 32
И я так тоже понимаю! ОК!
Ребята, спасибо! Исправлено
Автор статьи как я понимаю перепутал описание UTF-8 и UTF-32.