Особенности национального языка в программном коде [Олег Иванович Цилюрик] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Особенности национального языка в программном коде
Олег Цилюрик
Редакция 31, от 01.02.2023

Оглавление
От автора..........................................................................................................................................4
Структура текста........................................................................................................................4
Разметка и код.............................................................................................................................4
Пара слов про авторские права.................................................................................................4
Проблемы локализации (вместо предисловия)............................................................................5
Литература и сетевые ресурсы.................................................................................................7
Часть 1. Локализация в Linux........................................................................................................7
Интернационализация...............................................................................................................8
Символьные строки...................................................................................................................9
Представление текстовой информации...............................................................................9
Кодирование UTF-8.............................................................................................................10
Локализация строк в коде C/C++............................................................................................13
Язык C и локализация.........................................................................................................13
Примечание о примерах кода C/C++.................................................................................15
Строки в C/C++....................................................................................................................15
Локали и локализация.........................................................................................................18
Детали локализации в C..........................................................................................................19
API для работы со строками...............................................................................................21
Разрушение потоков ввода/вывода....................................................................................21
Некоторые примеры............................................................................................................23
Детали локализации в C++......................................................................................................26
Операции со строками........................................................................................................26
Потоки ввода-вывода локализованных символов............................................................26
Разрушение ориентации потоков.......................................................................................27
Некоторые современные языки..............................................................................................29
Python....................................................................................................................................29
Go..........................................................................................................................................31
Rust........................................................................................................................................32
Kotlin.....................................................................................................................................34
Сравнения, поиск, сортировки и другие ...............................................................................35
Операции над мультибайтными строками........................................................................35
Контейнеры STL широких символов.................................................................................36
Сортировки..........................................................................................................................39
Литература и сетевые ресурсы...............................................................................................42
Часть 2. Регулярные выражения в программном коде..............................................................43
Общие замечания относительно регулярных выражений....................................................43
Как это работает в утилитах GNU.....................................................................................45
Как это работает из программного кода............................................................................46
Регулярные выражения в C.....................................................................................................46
PCRE.....................................................................................................................................52
PCRE и POSIX нотация..................................................................................................56
Широкие символы Unicode................................................................................................58
Регулярные выражения в C++.................................................................................................60
Поздние языки программирования........................................................................................65

Python....................................................................................................................................65
Go..........................................................................................................................................68
Rust........................................................................................................................................71
Статическая компиляция................................................................................................74
Kotlin.....................................................................................................................................77
Запуск Kotlin программ..................................................................................................78
Использование регулярных выражений.................................................................................80
Литература и сетевые ресурсы...............................................................................................81

— Сейчас модно держать странных животных. Вон наш
участковый Кирюхин поимел себе коалу и теперь
эвкалипт высаживает на приусадебном участке. Коалы
страсть как эвкалиптовые листья любят. Кирюхин
пытался конечно эвкалиптовой настойкой пропитывать
наши отечественные березовые листья, но тот ни в
какую.
Кинофильм «Особенности национальной рыбалки»

От автора
Структура текста
Весь последующий текст состоит из 2-х частей, и разбит на подразделы. В первой части
рассматриваются общие вопросы представления и техники работы с текстовой информацией,
отличной от англоязычной. Сначала мы рассмотрим здесь вопросы локализации в языке C. Затем
то же повторно будет рассмотрено на языке C++. Это классика … и POSIX API. И только затем
вернёмся коротко к обзору современных (более поздних) языков программирования.
Основной упор далее будет сделан не на словесные описания, а на иллюстрации на примерах
фрагментов кода, которые не нуждаются в особых пояснениях. Соответственно, этот материал не
рассчитан на тех, кто первоначально изучает язык C (или C++, или любой другой), а предполагает
уже достаточно обстоятельное знание языков.
Во второй части рассматриваются вопросы работы с регулярными выражениями в языке C.
Совершенно естественно, что они в полной мере могут быть применимы и в C++. Затем повторно
будет рассмотрена специфика исключительно C++. И далее, как и раньше — более современные
языки.
В примерах здесь использованы только очень простые, вплоть до тривиальных, образцы
регулярных выражений. Это сделано сознательно для упрощения, и объясняется это тем, что
предметом нашего рассмотрения является не составление регулярных выражений и их синтаксис,
а поведение самих регулярных выражений с Unicode строками, когда понятия символ и байт
перестают быть тождественными.
Всё последующее изложение построено на стандартах POSIX и использовании исключительно
операционной системы Linux. К Windows, из-за совершенно отличного там представления
локализованных строк (Unicode, UTF-16), всё сказанное не относится вообще … кроме, разве что,
самых общих фактов о структуре строчных данных.

Разметка и код
Цитируемые из сторонних источников фрагменты в тексте выделяются курсивным шрифтом.
Протоколы выполнения команд и листинги программных кодов выделяются моноширинным
шрифтом. В примерах выполнения команд, как это часто делается, вывод программы (системы) на
терминал показывается обычным шрифтом, а пользовательский ввод с терминала пользователем
— жирным шрифтом.
Архив всех представленных в тексте примеров кода (с прилагаемыми файлами протоколов сборки,
изменений, выполнения, тестирования), чтобы не восстанавливать их из текста, может быть
свободно скачан по ссылкам в блоге автора: http://mylinuxprog.blogspot.com/2016/09/cc.html.
В примерах, как это часто делается в публикациях, вывод программы (системы) на терминал
показывается обычным шрифтом, а ввод с терминала пользователем — жирным шрифтом, иначе
в показанных потоках вывода крайне сложно уследить, что является исходными сроками, а что
результатами сопоставления с образцом.

Пара слов про авторские права
В заключение — относительно авторских прав. Ничто из представленного в этом тексте не
заимствовано ни из каких источников. Все представленные варианты решений — авторские, со
всеми возможными их ошибками и неточностями. Весь этот текст и все сопутствующие ему
программные коды предоставляется под лицензией Creative Commons Attribution ShareAlike
(«общественное достояние»), что означает:
... допускается копирование, коммерческое использование произведения, создание его
производных при чётком указании источника, но при том единственном ограничении, что
при использовании или переработке разрешается применять результат только на
условиях аналогичной лицензии.

Проблемы локализации (вместо предисловия)
Вселенная – некоторые называют её Библиотекой –
состоит из огромного, возможно, бесконечного числа
шестигранных галерей, с широкими вентиляционными
колодцами, ограждёнными невысокими перилами. Из
каждого шестигранника видно два верхних и два нижних
этажа – до бесконечности.
Хорхе Луис Борхес «Вавилонская Библиотека»
Предметом рассмотрения этих настоящих заметок являются вопросы использования
национальных языков (русского, арабского, китайского … любых отличных от английского) в
программном коде, главным образом для операционной системы Linux или подобных. Основное
внимание будет уделено традиционным языкам C/C++ (классика жанра), но и беглому обзору
состояния дел в современных, более новых языках программирования (Python, Go, Rust, Kotlin…)
будет также бегло уделено некоторое внимание.
Как очень скоро будет показано и станет понятно, что после внедрения в IT-практику представлений
(символов) Unicode, особенности использования самых разнообразных языков нивелировались и
становятся единообразными (алфавитных или иероглифических систем, или даже систем
специальных символов, таких как: математических или нотных символов). Важно чтобы такой набор
символов (алфавит) отображался в кодировке Unicode! Поэтому во всём дальнейшем мы можем и
будем везде употреблять термин «национальный язык» применительно не только к русскому
языку, но расширенно — к любому языку: арабскому, китайскому, японскому …, более того, даже к
языкам специальной нотации, например, к языку математических символов.
Вопрос использования национального языка в программном коде имеет несколько аспектов,
«многослойный», как минимум это (а может и больше):
1. Использование таких языков программирования, в синтаксисе которого ключевые и
зарезервированные слова допускаются (или предусмотрены только в таком виде) на
национальном языке.
2. Допускает ли язык программирования присваивание переменным (и любым другим объектам
программного кода) имён, записываемых на национальных языках.
3. Как производится представление, создание, хранение и ввод/вывод текстовых литералов
(строчных констант) на национальных языках. Представление текстовых литералов может
реализовываться либо «доморощенными» способами (поток байт: Pascal, ранний C), или
использованием Unicode (в той или иной кодировке: UTF-8, UTF-16, UTF-32) — это не имеет
значения.
4. Как производятся манипуляции с содержимым текстовых строк на национальных языках:
поиск, замена, перестановки и другие операции. Может показаться, что это то же самое что и
предыдущий пункт, но, как мы увидим вскоре, это принципиально разные вещи.
Все эти, а возможно, и другие стороны вопроса — это всё совсем не одно и то же! В любом языке
программирования, или его среде, инструментарии (библиотеки, пакеты, модули и др.), могут
реализовываться одни пункты, и вовсе не обеспечиваться другие.
Чтобы больше не обращаться к этому, остановимся очень коротко на п.1 этого перечисления. Это
вовсе не курьёз и совсем не такое пустое начинание: на раннем этапе развития того, что позже
стали обозначать как IT, в СССР разрабатывался целый ряд языков программирования с
русскоязычным синтаксисом ключевых слов-операторов. Причём таких языков программирования,
фундаментальные идеи которых потом повлияли на всё развития языков программирования в
последующие 40-50 лет. Поэтому стоило бы оглянуться, и коротко их хотя бы назвать:


Язык Рефал. Первая версия Рефала была создана в 1966 году Валентином Турчиным. Это
единственный язык из этого перечисления получивший мировую известность.



1968г. и далее: Институтом кибернетики Академии наук Украинской ССР, под рук. акад.

Глушкова В.М. разработаны и производились вычислительные машины МИР и МИР-2 (МИР
- это не претензии на Universe, а просто Машина Инженерных Расчётов), работающие по
программам на языке Алмир/Аналитик. Там оператор цикла мог выглядеть как-то так:
ДЛЯ Ж=1 ШАГ 0.3 ДО π ВЫПОЛНИТЬ ...



Новосибирск, 1970 - 1981г.г., под руководством акад. Ершов А.П. создаётся обучающая
система «Школьница» и языка Рапира, для там же созданного компьютера «Агат».
Пример программы «Здравствуй, мир!»:

ПРОЦ СТАРТ();
ВЫВОД: "ЗДРАВСТВУЙ, МИР!";
КНЦ;

Акад. Ершов А.П ещё в начале 80-х предполагал использовать лёгкость изучения языка
Рапира для достижения всеобщей компьютерной грамотности (в 1980-м году!).


Система программирования Бета: Изначально этот язык был назван — автокод Эльбрус,
затем был переименован в Эль-76.



Язык Сигма — название неожиданно очень удачно стало соответствовать сути
разработанного языка, которую можно описать как «Символьный Генератор и
Макроассемблер». Всего в истории языка Сигма было три его реализации: на М-20, на
БЭСМ-б и на самом языке Сигма.

И даже это ещё далеко не все...
Но русскоязычный синтаксис языка ничего принципиально не добавляет к семантике языка… На
ранних этапах становления понимания семантики языков программирования это было вполне
естественно. Язык программирования должен объединять разноязыких разработчиков, а не
разъединять по какому-то ни было признаку. И уже к середине 80-х годов это было понято, и
движение именно в этом направлении прекратилось… Так на этом мы и закончим краткий экскурс в
1-й из перечисленных аспектом поддержки национальных языков в программном коде. И к нему
далее не станем обращаться…
Краткое замечание о соотношении пунктов 2 и 3 перечислений: константное представление и
манипулирование содержимым строчных данных в коде. До тех пор, пока понятия символ и байт в
текстовом представлении были эквивалентными, понятия хранения и оперирования с такими
строками были тождественными. Но как только представление каждого символа в Unicode стало
возможным представлять 1, 2, 3, 4 байтами (а потенциально до 6) — эта тождественность
разрушается: если мы попытаемся заменить 3-байтовый символ в строке на 1-байтовый, то мы тут
же разрушим всю структуру текстовой строки (образуются зависшие «остатки» в виде 2-х байт). И
для корректного выполнения разнообразных манипуляций с контекстом строки нужно находить
адекватные методы. Об этом мы подробно станем говорить ниже...

Литература и сетевые ресурсы
1. Разработка языков программирования и компиляторов в СССР
https://habr.com/ru/company/ua-hosting/blog/273665/
2. Виктор Михайлович Глушков. Опережая время. 16 марта 2017.
https://habr.com/ru/company/ua-hosting/blog/370259/
3. Ершов, Андрей Петрович
https://ru.wikipedia.org/wiki/Ершов,_Андрей_Петрович

Часть 1. Локализация в Linux
Воспользуемся формулировками пусть и из самых поверхностных источников, Википедии
(https://ru.wikipedia.org/wiki/Локализация_программного_обеспечения):
Локализация программного обеспечения — процесс адаптации программного обеспечения к
культуре какой-либо страны. Как частность — перевод пользовательского интерфейса,
документации и сопутствующих файлов программного обеспечения с одного языка на другой.
Для локализации в английском языке иногда применяют сокращение «L10n», где буквы «L» и «n»
— начало и окончание слова Localization, а число 10 — количество букв между ними.
Во всех современных дистрибутивах Linux на сегодня по умолчанию используются локали,
построенные на UTF-8 кодировании символьных представлений Unicode, например:
$ locale
LANG=ru_UA.UTF-8
LANGUAGE=ru_UA:ru
LC_CTYPE="ru_UA.UTF-8"
LC_NUMERIC=ru_UA.UTF-8
LC_TIME=ru_UA.UTF-8
LC_COLLATE="ru_UA.UTF-8"
LC_MONETARY=ru_UA.UTF-8
LC_MESSAGES="ru_UA.UTF-8"
LC_PAPER=ru_UA.UTF-8
LC_NAME=ru_UA.UTF-8
LC_ADDRESS=ru_UA.UTF-8
LC_TELEPHONE=ru_UA.UTF-8
LC_MEASUREMENT=ru_UA.UTF-8
LC_IDENTIFICATION=ru_UA.UTF-8
LC_ALL=

Использование Unicode уже гарантировано не требует каких-то отдельных кодовых страниц для
различных национальных языков … как это требовалось в MS-DOS или Windows. Как видно из
показанного листинга, локализация в общем виде предусматривает много аспектов: национальные
представления денежных единиц, числовых величин, форматов даты и времени и др. … Нас в
дальнейшем рассмотрении будут интересовать только некоторые аспекты, главным образом
связанные с внутренним представлением текстовой информации в программе, и вопросы её
ввода и вывода на периферийные устройства1. Потому и текст посвящён «национальным языкам в
программном коде», а не «локализации в операционной системе» … кроме отдельных самых
кратких аспектов системной локализации
Например относительно устранения избыточных локалей :
$ lsb_release -a
No LSB modules are available.
Distributor ID:
Linuxmint
Description:
Linux Mint 21
Release:
21
Codename:
vanessa
$ locale -a | wc -l

1

То, что в эпоху MS-DOS называлось «руссификация», и над чем тогда много копий было сломано...

28
$ locale -a | head
C
C.UTF-8
en_AG
en_AG.utf8
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8

Или то же в Fedora:
$ lsb_release -a
LSB Version:
:core-4.1-amd64:core-4.1-noarch
Distributor ID:
Fedora
Description:
Fedora release 35 (Thirty Five)
Release:
35
Codename:
ThirtyFive
$ locale -a | wc -l
869
$ locale -a | head
aa_DJ
aa_DJ.iso88591
aa_DJ.utf8
aa_ER
aa_ER@saaho
aa_ER.utf8
aa_ER.utf8@saaho
aa_ET
aa_ET.utf8
af_ZA

Для инсталляционных дистрибутивов Linux присутствие всех возможных локалей — абсолютно
обязательно, это понятно. Но весь этот избыток локалей:


абсолютно избыточен в малых или встраиваемых конечных реализациях;



даже в десктопных рабочих станциях избыток локалей требует заметных временных
издержек при регулярных обновлениях инсталляции проводимых по сети;

В своей конкретной инсталляции вполне возможно оставить только минимум локалей необходимых
в вашем языковом окружении, например подобными командами:
$ sudo localedef --delete-from-archive en_ZA.utf8 en_ZM en_ZM.utf8 en_ZW.utf8

И, в конечном итоге, последовательностью таких удалений можно свести весь набор к:
$ locale -a
C
C.UTF-8
en_GB.utf8
en_US.utf8
POSIX
ru_RU.utf8
ru_UA.utf8

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

основных элементов (среди других) техники интернационализации является возможность
последующей загрузки локализированных элементов в будущем при желании пользователя2 (даже
если они отсутствуют на момент разработки).
Мы не будем дальше обращаться к этой технике, потому что это предмет совершенно других
интересов. Но интернационализация программных проектов в целом не отменяет проблемы
локализации, рассматриваемые далее. Программный код может получать потоки текстовой
информации из внешних, относительно самого приложения, источников, с не прогнозируемым
содержанием, и должен корректно работать с любым получаемым контекстом.
И если локализации посвящены репозиторные пакеты l10n, то интернационализации — пакеты i18:
$ aptitude search l10n | wc -l
239
$ aptitude search i18 | wc -l
70

Но ни то, ни другое, не есть предметом нашего интереса, и мы заниматься здесь этим не будем.

Символьные строки
В данный момент я лишь провожу инвентаризацию ... механически
выстраиваю эти детали в ряд. Но это вполне стоящее занятие –
постепенно, мало-помалу соединять реальность в единое целое.
Так от трения камней или кусочков дерева друг о друга в конце
концов выделяется тепло и появляется огонь. Это похоже на то,
как из набора на первый взгляд бессмысленных, однообразно
повторяющихся раз за разом звуков, складываются слоги…
Харуки Мураками «Хроники Заводной Птицы».
Здесь мы переходим к конкретным вопросам локализации текстовых строк в базовых и
традиционных языках программирования C (как основа API Linux) и C++ (как наследник C). К
состоянию дел в других современных языках программирования (Python, Go, Rust, Kotlin …) мы
ещё вернёмся в конце текста.

Представление текстовой информации
Мир, в который вы собираетесь вступить, не
имеет хорошей репутации.
Иосиф Бродский, речь перед
Мичиганского университета.

выпускниками

До некоторого времени (до начала 80-х) представление символьной информации базировалось,
главным образом, на 7-битовом кодировании каждого символа (ASCII). Такая кодировка
предполагала представление только основных латинских символов, цифровых символов и
символов пунктуации (точка, запятая, дефис и т.д.). Если нужно было перейти на другую кодировку
(тоже 7-бит), то в потоке байт-символов вставлялся символ перехода на другую кодировку ('\17' для
русских символов), а при возврате в исходную кодировку — символ возврата ('\18'). Такая техника
представления символов использовалась, например, в майнфреймах IBM (IBM-360, EC-1020), или
мини-компьютерах DEC (LSI-11, PDP-11, «Электроника 60», «Электроника 79»).
Позже (в IBM PC и MS-DOS) были введены 8-битовое (расширенное) кодирование символов и
кодовые страницы (исторически термин code page был введён корпорацией IBM). В таком варианте
первая половина каждой кодовой таблицы (коды 0-127) как и раньше представляла ASCII набор
символов (латинский алфавит), а вторая половина (код 128-255) — заполнялась алфавитом того
или иного национального алфавита (имеющих алфавитные системы письма). Так, для конкретики,
основная таблица, используемая для русского языка в MS DOS — CP866. В Windows для русского
языка используется таблица CP1251 (но могут быть и другие, например ISO 8859-5 и т.д.). В ранних
Linux (и других UNIX) в качестве русскоязычной кодовой страницы использовалась KOI-8R (но могут
2

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

быть и другие). Всё, связанное с использованием кодовых страниц, мы не будем затрагивать в
дальнейшем рассмотрении, как устаревший и отживший подход.
К началу 90-х годов всё расширяющееся число кодовых таблиц и порождаемая ними путаница всех
безумно достали — число таблиц становится неподъёмным, а многие из них вообще практически
не находят использования (так, например, кирилическая кодовая таблица ISO 8859-5 никогда не
использовалась в русскоговорящих странах, но её упорно использовали зарубежные производители
для «русской локализации»)… Кроме того, кодовые таблицы не позволяли покрыть языки с не
алфавитной системой письма. В итоге, в 1991 году был предложен стандарт представления
Unicode. Первый стандарт выпущен в 1991 году, последний — в 2016 (8.0.0), следующий ожидался
летом 2017 года. Коды в стандарте Юникод разделены на несколько областей (страниц). Область с
кодами от U+0000 до U+007F содержит символы набора ASCII с их соответствующими кодами. Под
символы кириллицы выделены области знаков с кодами от U+0400 до U+052F, от U+2DE0 до
U+2DFF, и от U+A640 до U+A69F.
В таблицы Unicode любой символ (будь то английского или китайского языка) выражается 32битным значением. И это и есть тип wchar_t, который имеет в UNIX/Linux размер 4 байта (не
путать с Windows, где wchar_t — это 2 байта, 16 бит). Но таблицы Unicode — это абстракция. А
для представления этих значений нужно их как-то кодировать. И для этого предложены несколько
систем кодирования: UTF-323 (это в чистом виде значения Unicode и wchar_t POSIX), UTF-16 (и
wchar_t Windows ... но это нам не интересно) и UTF-8 (байтное кодирование переменной длины,
которое было придумано всё теми же хорошо известными Кеном Томпсоном и Робом Пайком в
1992 году для ОС Plan 9). Любой существующий символ кодируется в UTF-8 последовательностью
от 1-го до 6-ти последовательных байт (типа char). Символы русского языка (кирилица)
отображаются в UTF-8 как 2 байта на символ, но это не следует рассматривать как твёрдое
правило или константу для всей строки: в едином потоке могут содержаться и латинские символы
(1 байт на символ) и специальные, диакритические или другие символы (3-4 байт на символ) 4.

Кодирование UTF-8
Прежде чем рассматривать мультибайтные строки в коде, хорошо бы рассмотреть детально
байтовую структуру кодируемых в UTF-8 символов, а для этого подготовить некоторые тестовые
наборы строк на разных национальных языках. Несмотря на то, что нам желательно записать
строки на самых замысловатых языках (алфавитных и иероглифических, с записью слева-направо
и справа-налево) именно в Linux это сделать относительно несложно (из-за того, что любая
текстовая информация в Linux представляется в UTF-8). Для этого можно, например, в переводчика
Google заказывать фразу на перевод на нужный язык, а затем, не понимая в том что записано как
результат, скопировать, в любом текстовом редакторе, этот результат в файл. В итоге у меня
получился тестовый файл:
$
1
2
3
4
5
6
7
8
9
A
B
C

cat mult.dat
Здравствуйте
‫السالم عليكم‬
|
Dobrý den
Hello
‫ש ָ ׁלו ֹם‬
नमस ्त े
こんにちは
今日は
안녕하세요
你好
Olá
Hola

|
|
|
|
|
|
|
|
|
|

Здесь каждая строка начинается с 1-символьного номера, чтобы на неё можно было ссылаться на
строку … другие небольшие странности формата тестовых строк будет объяснена очень скоро.
Здесь каждая строка представляет примерно одно и то же на разных языках:
3

4

Стандарт Unicode состоит из двух основных разделов: универсальный набор символов (UCS, Universal Character Set)
и семейство кодировок (UTF, Unicode Transformation Format). Универсальный набор символов задаёт однозначное
соответствие символов кодам — элементам кодового пространства, представляющим неотрицательные целые числа
(32-бит разрядности). Семейство кодировок определяет машинное представление последовательности кодов UCS.
Значения (uint32_t), закодированные в UTF-8, в принципе, могут быть длиной до 6 байт, однако стандарт Unicode не
определяет символов выше 0x10ffff, поэтому символы Unicode могут иметь максимальный размер в 4 байта в UTF-8.

1.
2.
3.
4.
5.
6.
7.
8.
9.
A.
B.
C.

Русский
‫« السالم عليك‬Салам алейкум» — с арабского переводится как «мир вам» или «мир с вами».
С чешского Dobrý den : добрый день, здравствуйте, доброе утро
Английский
Шалом ‫ ש ָ ׁלו ֹם‬: слово на иврите, означающее «мир», традиционное еврейское приветствие
Перевод с санкрита नमस ्त े : доброе утро, добрый день, здорово
Японское приветствие (konnichiwa): "приветствую вас", иероглифами на Хирагане: こんにちは
Японское приветствие (konnichiwa), иероглифами Катакана: 今日は
Перевод корейского 안녕하세요 на русский: добрый день, доброе утро, добрый вечер
Китайский, 你好 перевод на русский: привет, добрый день, здравствуйте
Испанский - Olá : самое распространенное и общепринятое приветствие.
Испанский - Hola : привет

Первый и простейший способ взглянуть на внутренне представление этих строк — это стандартная
утилита hexdump в байтовом отображении (не самый комфортный способ, но иногда вполне
достаточный):
$ hexdump
00000000
00000010
00000020
00000030
00000040
00000050
00000060
00000070
00000080
00000090
000000a0
000000b0
000000c0
000000d0
000000e0
000000f0
00000100
00000110
00000120
00000130
00000140
00000150
00000160
00000170
00000180

-C
31
d0
32
b9
33
20
34
20
35
20
36
a4
37
af
38
20
39
94
41
20
42
20
43
20

mult.dat
20 d0 97
b2 d1 83
20 d8 a7
d9 84 d9
20 44 6f
20 20 20
20 48 65
20 20 20
20 d7 a9
20 20 20
20 e0 a4
e0 a5 87
20 e3 81
20 20 20
20 e4 bb
20 20 20
20 ec 95
20 20 20
20 e4 bd
20 20 20
20 4f 6c
20 20 20
20 48 6f
20 20 20

d0
d0
d9
8a
62
20
6c
20
d6
20
a8
20
93
20
8a
20
88
20
a0
20
c3
20
6c
20

b4
b9
84
d9
72
20
6c
20
b8
20
e0
20
e3
20
e6
20
eb
20
e5
20
a1
20
61
20

d1
d1
d8
83
c3
20
6f
20
d7
20
a4
20
82
20
97
20
85
20
a5
20
20
20
20
20

80
82
b3
d9
bd
20
20
20
81
20
ae
20
93
20
a5
20
95
20
bd
20
20
20
20
20

d0
d0
d9
85
20
20
20
20
d7
20
e0
20
e3
20
e3
20
ed
20
20
20
20
20
20
20

b0
b5
84
20
64
20
20
20
9c
20
a4
20
81
20
81
20
95
20
20
20
20
20
20
20

d0
20
d8
20
65
20
20
20
d7
20
b8
20
ab
20
af
20
98
20
20
20
20
20
20
20

b2
20
a7
20
6e
20
20
20
95
20
e0
20
e3
20
20
20
ec
20
20
20
20
20
20
20

d1
20
d9
20
20
20
20
20
d6
20
a5
20
81
20
20
20
84
20
20
20
20
20
20
20

81
20
85
20
20
20
20
20
b9
20
8d
20
a1
20
20
20
b8
20
20
20
20
20
20
20

d1
7c
20
7c
20
7c
20
7c
d7
7c
e0
7c
e3
7c
20
7c
ec
7c
20
7c
20
7c
20
7c

82
0a
d8
0a
20
0a
20
0a
9d
0a
a4
0a
81
0a
20
0a
9a
0a
20
0a
20
0a
20
0a

|1 ..............|
|..........
|.|
|2 ............ .|
|.........
|.|
|3 Dobr.. den
|
|
|.|
|4 Hello
|
|
|.|
|5 ..............|
|
|.|
|6 ..............|
|....
|.|
|7 ..............|
|.
|.|
|8 .........
|
|
|.|
|9 ..............|
|.
|.|
|A ......
|
|
|.|
|B Ol..
|
|
|.|
|C Hola
|
|
|.|

(Здесь становится понятны и некоторые странности моего тестового файла: длина дополняется
пробелами так, чтобы каждая строка в символьном дампе, справа, начиналась с новой строки, а
символ ограничитель '|' — любой отчётливо различимый в дампе ограничитель строки).
Таким инструментом анализировать можно, но неудобно, поэтому была сделана небольшая
программ несколько более специализированного свойства:
$ cat ss1.c
#include
#include
#include
#include
#include
#include
#include
inline void c2w(char *c, wchar_t *w) {
int n = -1;
setlocale(LC_ALL, "");
// только после этого работают преобразования!
while (n != 0)

c += (n = mbtowc(w++, c, MB_CUR_MAX));
}
void show_str(char* pstr) {
wchar_t wbuf [strlen(pstr) * 2];
char *pb = pstr + strlen(pstr) - 1;
while(*pb == ' ') *pb-- = '\0';
// удаление хвостовых пробелов
c2w(pstr, wbuf);
printf("%s [%ld bytes:%ld symbols]\n",
pstr, strlen(pstr), wcslen(wbuf));
do
printf("", (uint8_t)*pstr++);
while (*pstr != '\0');
printf("\n");
}
int main( int argc, char **argv ) {
char buf[4096], *pb;
if (argc > 1) {
strcpy(buf, argv[1]);
show_str(buf);
}
else
while (1) {
if (NULL == fgets(buf, sizeof(buf) - 1, stdin)) break;
if (1 == strlen(buf)) break; // пустая строка
if (index(buf, '\n') != NULL) *index(buf, '\n') = '\0';
if (index(buf, '|') != NULL) *index(buf, '|') = '\0';
pb = buf;
if (' ' == *(pb + 1)) pb +=2; // убрать нумерацию строк
show_str(pb);
}
return 0;
}
$ gcc -Wall -O2 -o ss1

ss1.c

Программа позволяет рассмотреть как строку заданную в командной строке:
$ ./ss1 今日は
今日は [9 bytes:3 symbols]


Так и в диалоге с терминала:
$ ./ss1
你好
你好 [6 bytes:2 symbols]

नमस्ते
नमस्ते [18 bytes:6 symbols]


Или символы из алфавитов специального назначения — символы валют:
$ ./ss1 €₤₴₽
€₤₴₽ [12 bytes:4 symbols]


А если программа может получать строки из sysin, то таким образом может получать строки и из
любого текстового файла:
$ cat mult.dat | ./ss1
Здравствуйте [24 bytes:12 symbols]

‫[ السالم عليكم‬23 bytes:12 symbols]


Dobrý den [10 bytes:9 symbols]

Hello [5 bytes:5 symbols]

‫[ ש ָ ׁלו ֹם‬14 bytes:7 symbols]

नमस ्त े [18 bytes:6 symbols]

こんにちは [15 bytes:5 symbols]

今日は [9 bytes:3 symbols]

안녕하세요 [15 bytes:5 symbols]

你好 [6 bytes:2 symbols]

Olá [4 bytes:3 symbols]

Hola [4 bytes:4 symbols]


Программа отчётливо разделяет такие понятия как символ в строке и отдельные байты в
представлении этих символов. Число байт и символов в строке отчётливо не совпадают! В моём
тестовом примере мы наблюдаем символы, которые представляются 1-м, 2-мя и 3-мя байтами.
Ещё один простой и быстрый способ декодировать любую строку на национальном языке в
последовательность байт — это использование Python (версии 3), за счёт того, что Python по
спецификации языка использует кодировку UTF-8. В диалоговом режиме это может выглядеть так:
$ python
Python 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 'Здравствуйте'.encode()
b'\xd0\x97\xd0\xb4\xd1\x80\xd0\xb0\xd0\xb2\xd1\x81\xd1\x82\xd0\xb2\xd1\x83\xd0\xb9\xd1\x82\xd0\
xb5'
...

И символы алфавитов специального предназначения — например, а). музыкальные символы и б).
специальные математические символы и знаки:
...
>>> '𝄞𝄚𝅗𝅥𝄚𝅘𝅥𝅯
𝄚𝄚𝄚'.encode()
b'\xf0\x9d\x84\x9e\xf0\x9d\x84\x9a\xf0\x9d\x85\x9e\xf0\x9d\x84\x9a\xf0\x9d\x85\xa1\xf0\x9d\x84\
x9a\xf0\x9d\x84\x9a\xf0\x9d\x84\x9a'
>>> '𝔐𝛯𝜟∀∃∯'.encode()
b'\xf0\x9d\x94\x90\xf0\x9d\x9b\xaf\xf0\x9d\x9c\x9f\xe2\x88\x80\xe2\x88\x83\xe2\x88\xaf'
>>>
...

Но про использование Python мы поговорим отдельно позже.

Локализация строк в коде C/C++
Язык C и локализация
С тех пор, как в 1969—1973 годах язык C был разработан Деннисом Ритчи с коллегами, он остаётся
неизменно и успешно используемым. Главной причиной такого долголетия 5 является, несомненно,
то, что C является базовым языком написания операционных систем (для чего он, собственно, и
был придуман) семейства UNIX (POSIX совместимых), и в частности Linux. И до тех пор, пока будет

5

Все другие языки программирования тех периодов (Algol, FORTRAN, COBOL, PL/1 …) практически полностью
ушли из практического использования, или используются только в узко ограниченных областях применения,
определяемыми традициями их использования и наработанными отраслевыми библиотеками.

жив Linux (и Android как его младший клон) — до тех пор будет жив и язык C 67.
По языку C существует множество книг, учебников, учебных курсов (ещё бы, при такой биографии!).
Но, как ни странно, до сегодня лучшим руководством является оригинальная книга «Язык
программирования Си», написанная в 1978 году, которую написали Брайан Керниган и Деннис
Ритчи (легендарное руководство «K&R»), число изданий которой ведёт счёт уже на десятки. При
всём богатстве выбора, все сегодняшние студенты начинают изучение языка C именно с K&R.
Но научиться просто языку C для практического программирования — мало! Ещё 50% успеха
обеспечивает знание среды, окружения, основных библиотечных функций … которые по привычке
и терминологически неправильно называют стандартной библиотекой C. Набор таких библиотечных
функций, эволюционирующий в среде C, позже выкристаллизовался и формализовался в наборе
стандартов POSIX8.
Одной из слабо описанных частей языка C и стандартов API POSIX является проблема
локализации текстовых строк в коде C и C++. Она состоит в том, как прозрачно (независимо от
системы, настроек, конкретного декодирования в коде и т.д.) обрабатывать взаимодействие с
внешней (относительно программного кода) средой (терминал, файловая система, сеть, …) на
любых национальных языках … отличных от английского: русском, китайском, арабском, ...
Но … обратим внимание, что эта тема (работа с локализованными строками) почти не отражена
при всей обширности публикаций по языкам C и C++. На то есть целый ряд причин:


сам тип локализованных символов (wchar_t) появился в стандарте C89, но, в полной мере
с API поддержки и т.п., только в стандарте C99 ... относительно недавно (по крайней мере,
недавно, в сравнении с 45-летней историей C);



и, конечно, этот тип и всё, что связано с локализацией, не может даже упоминаться в
классической литературе по C периода его становления: K&R и т.п.;



все более поздние книги и учебники по C, те которые переводные, тоже практически
полностью обходят эту тему стороной ... их англоязычным авторам она совершенно не
интересна, не близка — оно им не актуально ... да они и сами этой части языка просто
слабо знают с ней не пересекаясь;



отечественные же, русскоязычные учебники (а здесь встречаются только учебные книги по
C, для студентов университетов, например ... кто же станет писать "не-учебник" по столь
древнему языку?) — здесь авторы-педагоги, не являющиеся практиками программной
разработки, сами также, главным образом, переписывают и пересказывают материал из
англоязычных изданий ... ну, ещё придумают десяток собственных примеров кода; но раз в
первоисточниках этого нет, то его и вообще нет в природе;



далеко не последнюю роль (а может даже ведущую) сыграло то обстоятельство, что
последние 25-30 лет основная масса русскоязычных практикующих программистов
работали успешно подавляющим образом в аутосорсинге, на зарубежных заказчиков, где
вопросы локализации не стояли;

Но картина и потребности времени радикально меняются … в связи с требованиями и объёмами
импортозамещения и создания оригинального локализованного программного обеспечения. А
также из-за свежих законодательных требований по использованию отечественных операционных
систем, все они из которых являются теми или иными клонами Linux. Ещё жёстче становятся эти
требования в условиях всё возрастающих санкций и эмбарго со стороны англо-саксонских (да и не
только) стран. И это давление определённо не будет ослабляться в ближайший десяток лет...
Всё последующее изложение построено на стандартах POSIX и использовании операционной
системы Linux. К Windows это относится косвенно, только в общих принципах и в той части,
которая совместима с POSIX … или там где это коротко оговорено явно особо.

6

7
8

В новых языках программирования (Go, Python и мн. др.) сами стандарты языка оговаривают представление
символьной информации в UTF-8 — там проблемы локализации гораздо проще. Но C (а также и C++) — это
достаточно старые языки, и всё, что касается локализации, пришлось в них вводить «на ходу», при эксплуатации,
более поздними стандартами.
В самые последние (2021-2022) годы был проделан (впервые за 50 лет UNIX и 30 лет Linux) успешный опыт
написание фрагментов кода ядра операционной системы (Linux) на новом языке надёжного системного
программирования Rust.
Linux использует набор системных API расширенный относительно классического POSIX, но эти расширения не
затрагивают область локализации.

Примечание о примерах кода C/C++
Основной иллюстрацией рассматриваемых положений — это примеры кода. Предполагается
показать много, но очень не крупных примеров. Поэтому, только отдельные, наиболее
обстоятельные примеры будут показаны как отдельные законченые приложения, а вот набор
простейших мини-тестов разумно оказалось свести в единое приложение (unicode.c), в котором
определяется целый набор последовательных функций-тестов примерно вот такого вида:
void test00(void) { /* тест № 0 */
...
}
void test01(void) { /* тест № 1 */
...
}
...
void (*tests[])(void) = {
// последовательность тестов
test00, test01, test02,
test03, test04, test05,
test06, test07, test08,
test09, test10, test11,
test12, test13, test14,
/* ... */
};
static void do_test(int i) {
printf("%02d ---------------------------------------\n", i);
stdout = freopen(NULL, "w", stdout);
tests[i]();
stdout = freopen(NULL, "w", stdout);
}
int main(int argc, char **argv, char **envp) {
int i, j;
for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++)
if (1 == argc)
do_test(i);
else
for (j = 0; j < argc - 1; j++)
if (atoi(argv[j + 1]) == i)
do_test(i);
printf("------------------------------------------\n");
return 0;
}

Назначение строк вида stdout = freopen(…) будет объяснено вскорости.
Такая структура последовательности мини-тестов позволяет: а). избежать загруженности
отдельными однотипными приложениями, б). позволяет крайне легко добавлять код новых тестов в
общий набор тестов, и в). при запуске выполнять либо всю последовательность помещённых
тестов, либо указать только выборочные из них.
Вот так выполняются вся последовательность размещённых тестов:
$ ./unicode
...

А вот так может выполняться (отлаживаться) только выборочный набор из этих тестов:
$ ./unicode 1 3 5 6
...

Строки в C/C++
Представление и обработка строчной информации — это отчётливо слабая сторона языка C, и не
самая сильная сторона C++. Для проектов, предполагающий активные контекстные операции с

текстовыми строками, лучше применить языки, гораздо лучше для того предназначенные: Perl,
Python, Ruby, Go … в конце концов, bash.
Для внутреннего представления символьных строк, со времён ранних разработок в языках
программирования (да и в других IT инструментах, например системах управления базами данных)
было придумано только 2 принципиально различающихся способа: 1). последовательность
символов, которой предшествует поле длины строки, числа последующих дальше символов и 2).
последовательность символов, заканчивающаяся (ограниченная) символом, значения которого
заведомо не может быть в составе строке (терминальным символом, обычно со значением 0 —
поэтому этому значению ни в одной кодовой таблице не соответствовал никакой символ). 1-й
способ получил известность как Pascal-строки, второй — как C-строки.
(Попутно отметим, и это только подчёркивает общность этих методов, что точно такие же 2 способа
доступны для формата и разбиения «сообщений» в протоколе TCP/IP — в TCP нет никаких пакетов
или дейтаграмм, TCP — это поток байт, и строки или сообщения в нём нужно формировать
некоторым искусственным способом.)
В классическом C строки представляются просто как массив последовательных байт,
отображающих символы (1 символ — 1 байт). По соглашению, завершением строки является байт с
нулевым численным значением (этот символ-ограничитель не включается в состав строки, в её
длину). Этим соглашением строки, вообще то говоря, разграничиваются с массивами вообще,
например, с такими же массивами байт (массив байт, например, может иметь длину, ёмкость 100, а
размещённая в нём текущая строка — длину 10 байт-символов, со значением 11-го элемента '\0'):
char array[100] = "123456789A";

Классически, во всех книгах по C, символьные строки представляются как массив char,
символьные константы заключаются в двойные кавычки ("this is a string"), а отдельные
символы — в одиночные кавычки ('R'). Такие строки ещё обозначают аббревиатурой ASCIIZ,
подчёркивая, что это строка исключительно ASCII символов, завершающаяся нулевым байтом
(Zero).
Размер char представляется байтом, хотя в разных реализациях (операционных систем) char
может варьироваться как знаковое или беззнаковое байтовое значение (или указываться явно:
unsigned char или signed char)… но для наших дальнейших целей это не существенно. Для
работы со строками C стандарт POSIX определяет очень большой набор API (файл )
— набор функций разнообразной строчной обработки, многие из которых (но не все) имеют вид
str*().
Такие же строки (массивы) байт могут содержать (хранить) и мультибайтные последовательности
локализованных символов, представленных в кодировке UTF-8 (принятой во всех современных
реализациях Linux). При этом содержимое одного какого-то отдельно взятого, вычленненого байта
в этой строке может быть полной бессмыслицей в терминологии «символов», например, байт со
значением 0xD0. Контекстная обработка (по содержимому) таких строк, классическими строчными
функциями, для некоторых операций будет не корректной (и мы остановимся на этом подробно
позже).
Посчитайте символы, байты и байт на символ в последнем, иероглифическом примере:
void test00( void ) {
printf( "размер символа wchar_t вашей реализации = %ld байт\n", sizeof( wchar_t ) );
}
void test01( void ) {
char str[] = "Привет по-русски!";
printf( "%s [%ld байт]\n", str, strlen( str ) );
}
void test02( void ) {
char str[] = "Hello, 世界";
printf( "%s [%ld байт]\n", str, strlen( str ) );
}
$ ./unicode 0
00 --------------------------------------размер символа wchar_t вашей реализации = 4 байт
-----------------------------------------$ ./unicode 1
01 ---------------------------------------

Привет по-русски![31 байт]
-----------------------------------------$ ./unicode 2
02 --------------------------------------Hello, 世界 [13 байт]
------------------------------------------

Во всех современных дистрибутивах9 Linux всё представление текстовой информации делается в
кодировке UTF-8: текстовые файлы, файлы конфигурации, текстовые строки, настройки по
умолчанию в текстовых редакторах и т.д. и т.п. (так же, как это имеет место в ОС Plan 9 или в более
новых языках, например Python или Go, но C/C++ — это весьма старые языки). Когда вы набираете
свой программный код C/C++ в своём любимом текстовом редакторе (или IDE), то вы уже тем
самым вводите все символьные константы (то, что заключено в кавычки) в кодировке UTF-8 ...
даже если вы набираете англоязычную строку "xyz" из примера в K&R 1979 года издания (когда
ещё никто ничего не слышал про локализацию и Unicode).
Вы, конечно, можете перенастроить свой любимый текстовый редактор (или IDE), указав ему в
качестве кодировки какую-то глупость... типа CP-866 или CP-1251 (и большинство редакторов такое
позволяют сделать). И компилятор благополучно съест это, и на этапе выполнения функция
printf() будет благополучно выводить это на терминал (или в файл), потому что функции ввода и
вывода C никак не анализируют поток байт на принадлежность множеству допустимых символов, а
тупо выводят байт за байтом в поток. Но на этапе выполнения вы будете при этом иметь большие
хлопоты с визуализацией результатов (это будут не читаемые «кракозябры»)..., а если кто когда-то
позже вздумает работать с этим вашим кодом, то поминать он вас будет такими словами, что в
гробу вас будет крутить как пропеллер. Такое извращение может быть допустимо, но только только
для очень специальных целей, например, подготовки кода для переноса в другую операционную
систему. Таким образом, в итоге, символьные константы в текстовом файле, содержащем
программный код C/C++, могут быть записаны в любой кодировке, которую вы использовали при
подготовке этого кода, но везде в дальнейшем рассмотрении мы будем полагать, что эта кодировка
— UTF-810.
Для представления же и обработки локализованных строк (национальных языков) позже
(стандартом C89, а окончательно C99) был введен тип локализованных, широких (wide) символов
wchar_t. Это абстрактный тип данных, не привязанный стандартом к какому-то фиксированному
размеру. Но в POSIX/UNIX/Linux этот тип представляется 4-х байтным значением. В ОС Windows,
использующей такое устаревшее представление Unicode как UTF-16, тип wchar_t имеет размер 2
байта11. Как бы там ни было, никогда не следует в своём коде делать неявные предположение о
размере символа wchar_t.
Для записей широких символьных констант используется префикс-квалификатор: L"this is a
string". Точно так же обозначается и отдельный широкий символ: L'R'. (Эти примеры
показывают, что как широкие символы могут записываться и англоязычные строки, но они при этом
будут радикально отличаться содержанием от ASCIIZ строк того же написания.)
Так же, как и ASCIIZ, широкие строки завершаются нулевым значением типа wchar_t (но
имеющим в этом случае размер 4 байт — L'\0') — это не байт'\0'.
Язык C++ наследует все символьные представления своего предшественника C. Но, кроме того,
библиотеки C++ вводят (заголовочный файл ) новое объектное представление строк:
класс (тип) string — шаблонное (template) представление массива char динамического размера
(который можно понимать как тип vector). Для объектов этого класса определено
множество методов обработки, которые будут многократно продемонстрированы далее. Сейчас
для нас важно то, что string — это динамические массивы элементов типа char, и они точно так
9

Ещё не так много лет назад это было не так, и дистрибутивы Linux использовали по умолчанию 8-битовое
представление символов в выбранной кодовой странице, например в KOI-8R для русскоязычного окружения.
10 Это очень напоминает ответ Генри Форда на замечания относительно цвета его автомобилей «Форд-Т»: «Цвет
автомобиля может быть любым, при условии, что он черный».
11 Использование старого Unicode представления UTF-16 уже породило ряд проблем. Во-первых, из-за давно известной
разницы в порядке байт (little endian и big endian), представляющих 16-бит целое, понадобилось вводить метку
порядка байт (U+FEFF), а позже и кодировки размножились до различающихся UTF-16LE и UTF-16BE. Но хуже
того, во-вторых, что со временем набор Unicode расширился, и 16 бит стало недостаточно для его полного
представления. Тогда потребовалось введение суррогатных пар — кодирование символа двумя словами. И в одних
версиях (Windows 7 или 8) такие символы распознаются, а в других (Windows 95 или XP) — нет, и это причина
непереносимости. К счастью, при кодировании UTF-8 в POSIX системах таких проблем вообще не возникает, и
больше на них мы не будем обращать внимания.

же не пригодны для контекстной обработки локализованных строк, как и char[]. (Но они вполне
могут использоваться для хранения локализованных строк представленных кодированием UTF-8).
Аналогично, как для C тип wchar_t, для работы с локализованными текстами C++ вводит (файл
) класс (тип) wstring — динамические массивы элементов типа wchar_t
(vector).
Наконец, для взаимных преобразований мультибайтных (переменной длины) представлений
символов и строк UTF-8 и широких локализованных символов UTF-32 (wchar_t) стандартная
библиотека C (стандарт C99) вводит целую группу функций с именами вида *mb*() (multi bytes):
mblen(), mbtowc(), mbstowcs(), wcstombs() и т. д. Их использование позволяет (и не
предполагает) не ковыряться с внутренним побайтным UTF-8 представлением символов разных
языковых страниц.
Всё это, пунктиром обозначенное в этой части, будет неоднократно и детально проиллюстрировано
далее многочисленными примерами кода.

Локали и локализация
Для ввода/вывода ваш программный код должен знать локаль того устройства, с которым
осуществляются операции ввода-вывода (локаль, локализация — это более обширное понятие, но
нас будут интересовать только языковые настройки). Но кроме того, программа (используемые
языковые библиотеки) может иметь свою собственную локализацию, установленную по умолчанию
(при старте программы main()):
$ ./unicode 3
03 --------------------------------------локаль программы по умолчанию: C
------------------------------------------

Программы на C (и C++) устанавливают по умолчанию такую локализацию. Но локализация C или
POSIX устанавливают 7-битное кодирование, способное представлять только основную таблицу
ASCII: 0-127 (принятое на 70-е годы ХХ века, тот набор символов, которые могут встречаться в
самой записи кода программы на C). Ни о каком интернациональном вводе/выводе в таком случае
не может быть и речи, независимо от того, какая локализация (по умолчанию) установлена в
операционной системе.
Для того, чтобы иметь возможность локализованного ввода/вывода кодом C/C++, необходимо
установить в коде подходящую локаль — либо установленную по умолчанию в операционной
системе, либо принудительно одну из тех, которые инсталлированы в этой операционной системе.
Локаль операционной системы по умолчанию:
$ locale
LANG=ru_RU.utf8
LC_CTYPE="ru_RU.utf8"
LC_NUMERIC="ru_RU.utf8"
LC_TIME="ru_RU.utf8"
LC_COLLATE="ru_RU.utf8"
LC_MONETARY="ru_RU.utf8"
LC_MESSAGES="ru_RU.utf8"
LC_PAPER="ru_RU.utf8"
LC_NAME="ru_RU.utf8"
LC_ADDRESS="ru_RU.utf8"
LC_TELEPHONE="ru_RU.utf8"
LC_MEASUREMENT="ru_RU.utf8"
LC_IDENTIFICATION="ru_RU.utf8"
LC_ALL=

Набор локалей, которые вообще инсталлированы в конкретной операционной системе:
$ locale -a | grep ru
ru_RU
ru_RU.iso88595
ru_RU.koi8r
ru_RU.utf8

russian
ru_UA
ru_UA.koi8u
ru_UA.utf8

Полное число установленных локалей может быть очень значительным (поэтому выше показана
только небольшая часть их):
$ locale -a | wc -l
817

Эти команды крайне полезны для правильной записи локали в коде C/C++. При ошибке в записи
(строки) локали возникнет ошибка установка локали в C (и возбуждение исключения в C++),
текущая локаль программы при этом не изменится.

Детали локализации в C
В принципе, если вы собираетесь только хранить и выводить локализованные символьные строки
(константы), не анализируя или трансформируя их содержимое (контекст), то вы можете вообще не
заморачиваться с локализацией: многобойтные последовательности русских литер (в UTF-8) будут
корректно копироваться, переноситься или отображаться. Повторим показанный уже ранее пример:
void test01( void ) {
char str[] = "Привет по-русски!";
printf("%s [%ld байт]\n", str, strlen(str));
}
$ ./unicode 1
01 --------------------------------------Привет по-русски! [31 байт]
------------------------------------------

При этом нужно быть готовым к тому, что число символов в строке выше 17, но число байт в
строке будет 31 (результат возвращаемый strlen() как длина строки). Поэтому работать не
принимая во внимание локализацию можно, но при этом нужно соблюдать осторожность. Что
происходит, если самонадеянно не задумываясь использовать строки char[] для представления
русскоязычных (и любых других иноязычных) строк, легко увидеть проанализировав работу
функции побайтового реверса строк, сравнив результаты для русскоязычной и англоязычной
строк:
static char* revb(char *s) {
int i, j;
for(i = 0, j = strlen( s ) - 1; i utsrqponmlkjihgfedcba
абвгдеёжзийклмнопрсту => �тсрѿонмлкйизжБѵдгвба�
------------------------------------------

Как только наш код начинает анализировать или изменять содержимое строки, нам необходимо
работать с только с локализованными строками (строками широких символов wchar_t[]).

Первейшим действием программы мы должны установить () подходящую локаль (при
ненадлежащим образом указанной локали все преобразования между char[] и wchar_t[] в
обоих направлениях будут ошибочными).
Это может быть принудительно указанная локаль:
void test04(void) {
char *loc = setlocale(LC_CTYPE, "ru_RU.utf8");
if(NULL == loc) perror("locale error");
else fprintf(stdout, "локализация: %s\n", loc);
}
$ ./unicode 4
04 --------------------------------------локализация: ru_RU.utf8
------------------------------------------

Или это может быть локаль но умолчанию, устанавливаемая переменной окружения LANG:
void test05(void) {
char *loc = setlocale(LC_ALL, ""); // по умолчанию ("") - из $LANG
fprintf(stdout, "локализация: %s\n", loc);
}
$ echo $LANG
ru_RU.utf8
$ ./unicode 5
05 --------------------------------------локализация: ru_RU.utf8
-----------------------------------------$ LANG=japanese.euc; ./unicode 5
05 --------------------------------------локализация: japanese.euc
------------------------------------------

Примечание: Почему необходимо в программе устанавливать setlocale() в коде C и C++,
например при преобразовании мультибайтного представления (UTF-8) в широкие символы
wchar_t (UTF-32)? Это достаточно интересный вопрос если вспомнить, что: а). 4-х байтовое на
символ представление Unicode (в понимании Linux) однозначно определяет как кодовую страницу
(язык) так и код символа в этой таблице, а б). UTF-8 кодирование (от 1 до 6 байт на символ)
однозначно соответствует символу Unicode.
Дело в том, что традиционно программа C/C++ сама и по умолчанию устанавливает локаль "C" или
"POSIX" (так говорит стандарт POSIX). Это установилось много-много лет назад, и в такой локали
не может быть никаких многобайтных символов UTF-8, в ней отображаются только 7-бит ASCII
символы (так было ещё на компьютерах семейства PDP, на которых первоначально
отрабатывались и язык C и операционная система UNIX). В любой UTF-8 локали, независимо от
языковой локализации, все преобразования с любыми языками будут выполняться корректно. Что и
показывает нам пример:
void test14(void) {
printf("locale: %s\n", setlocale(LC_ALL, NULL));
setlocale(LC_CTYPE,"en_US.utf8");
printf("%ls : %s\n", L"русская строка в локали", setlocale(LC_CTYPE, NULL));
}
$ ./unicode 14
14 --------------------------------------locale: C
русская строка в локали : en_US.utf8
------------------------------------------

Таким образом, выполняя setlocale() в коде C/C++, мы не сколько устанавливаем нужную нам

локаль, сколько восстанавливаем символьное представление UTF-8, по умолчанию используемое
во всех современных дистрибутивах Linux.

API для работы со строками
Для традиционных строк C предоставляется () очень большое число функций для
работы со строками вида str*() и подобные им (memmove() и др.) — на все случаи жизни.
Относительно операций со строками C важно напомнить вот такие правила, которые очень часто
забывают и нарушают начинающие программисты, и которые поэтому стоит напомнить:
1. Строки char[] (char*) нельзя присваивать. В C операция присваивания (=) — это
копирование значения (даже для агрегатных переменных с типом struct {...}). Для
копирования значений строк предназначен целый ряд функций группы: strcpy(),
strncpy(), memcpy(), memmove(), …
2. Строки нельзя сравнивать (операциями ==, и т. п.). Для сравнения строк вводится
операция strcmp() (и strncmp()), которая возвращает результат лексографического
сравнения — целое число, которое меньше, больше нуля или равно нулю, если одна строка
соответственно предшествует (меньше), следует(больше) или равна другой строке, с
которой сравнивается.
Для строк широких (локализованных) символов определён () практически полностью
эквивалентный12 набор таких же функций, имеющих вид wcs*(). Например, вызову strlen()
сопоставлен вызов wcslen(), strsncpy() сопоставлен wcsncpy(), strcat() сопоставлен
wcscat(), memmove() сопоставлен wmemmove() и т.д.
Кроме того, определён (стандартом C99) целый ряд функций для работы с мультибайтными
последовательностями (UTF-8), взаимными преобразования между ними и широкими символами
(mbtowc(), mblen(), mbstowcs(), wcstombs() и др.). Это механизм взаимных преобразований
между char[] и wchar_t[].
Функции ввода/вывода дополнены () эквивалентами относительно работы с байтовыми
строками: fputws() (эквивалент fputs()), fputwc() (эквивалент fputc()), и так далее:
getwchar(), fgetwc(), fgetws() и т.д.
Наконец, API форматного ввода вывода (printf(), sprintf(), scanf() и т.д.) получили (C99)
новый формат для широких локализованных строк: если для байтовый строк используется формат
%s, то для широких строк — формат %ls.
Это краткого обзора API строк широких символов вполне достаточно для работы с
локализованными строками. Тонкие детали использования функций этого API можно получить из
man-страниц, которые предоставлены по всем таким функциям.

Разрушение потоков ввода/вывода
Начнём выводить в выводной поток (это наиболее наглядно) традиционные и широкие символы:
void test10(void) {
setlocale(LC_ALL, "");
char cs[] = "c-строка";
wchar_t ws[] = L"w-строка";
printf("%s\n", cs);
printf("%ls\n", ws);
printf("%s\n", cs);
printf("%ls\n", ws);
}

Казалось бы, что у нас попеременно в выходной поток пишутся и традиционные и широкие строки:
$ ./unicode 10
10 ---------------------------------------

12 Чтоб это не было неожиданностью — не совсем для всех функций srt*() вы найдёте прямой аналог wcs*(). Для
2-х подобных вызовов strtok() и strtok_r() (потоково-безопасный вариант, не вовлекающий в работу
статических переменных), представлен только 1 эквивалент с 3-мя параметрами (эквивалентный именно
strtok_r()): wcstok(wchar_t*, const wchar_t*, wchar_t**).

c-строка
w-строка
c-строка
w-строка
------------------------------------------

Но это дорогостоящее заблуждение! (в смысле поиска такой ошибки в более-менее объёмном
проекте). Сделаем два симметричных тестовых приложений:
void test11( void ) {
int res;
char cs[] = "c-строка\n";
wchar_t ws[] = L"w-строка\n";
setlocale( LC_ALL, "" );
res = fputws( ws, stdout );
printf( "%d: %m\n", res );
res = fputs( cs, stdout );
printf( "%d: %m\n", res );
res = fputws( ws, stdout );
printf( "%d: %m\n", res );
res = fputs( cs, stdout );
printf( "%d: %m\n", res );
}
void test12(void) {
int res;
char cs[] = "c-строка\n";
wchar_t ws[] = L"w-строка\n";
setlocale(LC_ALL, "");
res = fputs(cs, stdout);
printf("%d: %m\n", res);
res = fputws(ws, stdout);
printf("%d: %m\n", res);
res = fputs(cs, stdout);
printf("%d: %m\n", res);
res = fputws(ws, stdout);
printf("%d: %m\n", res);
}

Результаты их выполнения оказываются обескураживающе различающимися:
$ ./unicode 11 12
11 --------------------------------------w-строка
w-строка
12 --------------------------------------c-строка
1: Выполнено
-1: Выполнено
c-строка
1: Выполнено
-1: Выполнено
------------------------------------------

Выходной поток sysout (и любой другой поток: sysin, FILE* …) разрушается если в него
чередуется (хотя бы один раз) вывод традиционных и широких строк (да так, что в первом из
показанных тестов printf() не может вывести даже сообщение о произошедшей ошибке!). Об
этом явно сказано в С++ документации API и чуть позже мы зацитируем эту фразу,
когда дойдём непосредственно к C++.
Как же тогда выполнить, если необходимо, чередующийся вывод (или ввод) традиционных и
широких символов в один поток? Нужно переоткрыть поток! Так:

stdout = freopen( "/dev/stdout", "w", stdout );

Или даже так, ещё проще:
stdout = freopen( NULL, "w", stdout );

И тогда показанный тест примет такой, например, вид:
void test13(void) {
int res;
char cs[] = "c-строка\n";
wchar_t ws[] = L"w-строка\n";
setlocale(LC_ALL, "");
res = fputs(cs, stdout);
printf(" %d: %m\n", res);
stdout = freopen(NULL, "w", stdout);
res = fputws(ws, stdout);
stdout = freopen(NULL, "w", stdout);
printf(" %d: %m\n", res);
res = fputs(cs, stdout);
printf(" %d: %m\n", res);
stdout = freopen(NULL, "w", stdout);
res = fputws(ws, stdout);
stdout = freopen(NULL, "w", stdout);
printf(" %d: %m\n", res);
}
$ ./unicode 13
13 --------------------------------------c-строка
1: Выполнено
w-строка
1: Выполнено
c-строка
1: Выполнено
w-строка
1: Выполнено
------------------------------------------

А как же printf() в одном из тестов выше (test10()) спросите вы? … когда вперемешку
выводились и традиционные и широкие символы (даже в едином вызове printf()). Но printf()
— это библиотечный вызов, не системный (он описан в секции 3 man, а не 2). Он
последовательно вызывает библиотечный sprintf() и, затем, системный write( 1, ... ) для
вывода в stdout. После sprintf() (форматирования строки вывода) и формата %ls — в строке
подлежащей выводу нет уже никаких wchar_t, там только мултибайтные UTF-8 цепочки char,
поэтому проблем и не возникает. Но если поток вывода разрушен (для char) ранее выполненным
вызовом fputws(), то уже и строка char[], подготовленная sprintf() не может быть выведена.

Некоторые примеры
Посимвольное преобразование русскоязычной строки, представленной ASCIIZ в мультибайтной
кодировке UTF-8 в строку широких локализованных символов:
#define LENGTH 160
char
buf [LENGTH] = "тестовая русскоязычная строка в UTF-8 с прямым порядком слов ";
wchar_t wbuf [LENGTH];
//----------------------------------------------------inline void c2w(char *c, wchar_t *w) {
int n = -1;
setlocale(LC_ALL, ""); // только после этого работают преобразования!
While (n != 0)
c += (n = mbtowc(w++, c, MB_CUR_MAX));
}
void test07(void) {

printf("преобразование UTF-8 символов в широкие (wchar_t):\n");
printf("строка UTF-8 до преобразования: '%s'\n"
"длина UTF-8 строки = %d байт\n",
buf, (int)strlen(buf) );
c2w(buf, wbuf);
printf("локаль программы установлена: %s\n",
setlocale(LC_ALL, NULL));
printf("преобразованная строка: '%ls'\n"
"длина преобразованной строки = %d символов (%ld байт)\n",
wbuf, (int)wcslen(wbuf),
wcslen(wbuf) * sizeof(wchar_t));
}

Здесь MB_CUR_MAX — это константа, максимальная длина в байтах на символ в выбранной
локали, и может иметь значения до 6-ти.
$ ./unicode 7
07 --------------------------------------преобразование UTF-8 символов в широкие (wchar_t):
строка UTF-8 до преобразования: 'тестовая русскоязычная
'
длина UTF-8 строки = 110 байт
локаль программы установлена: ru_RU.utf8
преобразованная строка: 'тестовая русскоязычная строка
длина преобразованной строки = 63 символов (252 байт)
------------------------------------------

строка

в UTF-8 с прямым порядком слов

в UTF-8 с прямым порядком слов '

Обратное преобразование полученной строки (wchar_t[]) в форму UTF-8. Если в предыдущем
примере мы преобразовывали строки посимвольно (mbtowc()) в цикле, то теперь используем
функции, преобразующие целиком всю строку (wcstombs()):
void test08(void) {
int n;
c2w(buf, wbuf);
printf("обратное преобразование в UTF-8: %d байт\n", n = wcstombs(NULL, wbuf, 0));
wcstombs(buf, wbuf, n + 1); // с завершающим нулём
printf("преобразованная UTF-8 строка: '%s'\n", buf);
}
$ ./unicode 8
08 --------------------------------------обратное преобразование в UTF-8: 110 байт
преобразованная UTF-8 строка: 'тестовая русскоязычная
------------------------------------------

строка

в UTF-8 с прямым порядком слов '

Реверс русскоязычных слов, составляющих фразу. Здесь уже работает анализ и трансформация
содержимого локализованного текста:
void revers(wchar_t *w) {
wchar_t *sec, wb[40];
if(NULL == (sec = wcschr(w, L' '))) return;
wcsncpy(wb, w, sec - w)[sec - w] = L'\0';
while(L' ' == *sec) sec++;
revers(sec);
wcscat(wcscat(wmemmove(w, sec, wcslen(sec) + 1), L" "), wb);
}
void test09(void) {
c2w(buf, wbuf);
while(L' ' == wbuf[wcslen(wbuf) - 1])
wbuf[wcslen(wbuf) - 1] = L'\0';
printf("устранение завершающих пробелов: '%ls'\n", wbuf);
revers(wbuf);

printf("реверсирование слов: '%ls'\n", wbuf);
revers(wbuf );
printf("реверсирование слов: '%ls'\n", wbuf);
}
$ ./unicode 9
09 --------------------------------------устранение завершающих пробелов: 'тестовая русскоязычная строка в UTF-8 с прямым порядком
слов'
реверсирование слов: 'слов порядком прямым с UTF-8 в строка русскоязычная тестовая'
реверсирование слов: 'тестовая русскоязычная строка в UTF-8 с прямым порядком слов'
------------------------------------------

Здесь, для контроля достоверности, мы делаем 2 последовательных реверса (прямой и обратный),
чтобы в результате восстановить первоначальный вид строки.
Следующим примером мы сделаем чтение из файла и вывод на терминал локализованных
(кирилических) строк. Пишем программу, которая читает из файлов и русскоязычные и
англоязычные строки (с одинаковым успехом) и корректно выводит их содержимое на экран.
Чтение из файла производим сразу в строку широких символов (wchar_t, Unicode):
#include
#include
#include
#include






#define MAX_TXT 100
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("нужно указать имя входного файла\n");
return 1;
}
char *loc = setlocale(LC_ALL, "ru_RU.utf8"); // char *loc = setlocale(LC_ALL, "");
printf("локализация %s\n", loc);
FILE *fi = fopen(argv[1], "r");
if (!fi) {
printf("ошибка открытия файла %s: %m\n", argv[1]);
return 1;
}
wchar_t buf[MAX_TXT];
do {
if (NULL == fgetws(buf, MAX_TXT, fi)) {
if (feof(fi) != 0) break;
//EOF
printf("ошибка чтения: %m\n");
return 1;
}
printf("%ls", buf);
} while (0 == feof(fi));
fclose(fi);
return 0;
}

Программа с одинаковым успехом читает и русские и английские тексты:
$ ./utype r1.txt
локализация ru_RU.utf8
тестовая строка русского текста
$ ./utype e1.txt
локализация ru_RU.utf8
test string in English

Обращаем внимание на то, что строки входного файла, записанные в кодировке UTF-8,
считываются в строку wchar_t buf[] без каких либо явных преобразований в коде через функции
мультибайтных строк mb*(). Работа с потоками Unicode-строк будет корректной только после

установки локализаци setlocale(LC_ALL, "ru_RU.utf8").
Обращаем внимание на формат вывода широких строк printf("%ls", ...), причём (важно!) в
списке элементов вывода printf() могут вперемешку стоять как широкие строки, так и обычные
ASCII строки, каждые со своими, естественно, соответствующими форматами ("%ls" и "%s").

Детали локализации в C++
Операции со строками
C++, понятно, наследует все возможности C относительно строк, представляемых как массивы
char[] и wchat_t[]. Но C++ вводит новое (и предпочтительнее) объектной представление строк
string и wstring. Большая часть операций со строками, реализующиеся в C функциями API,
реализуются для объектов этих классов функциями-методами, за исключением вот таких важных
особенностей и отличий от строк в стиле C:
1. Строки C++ можно присваивать операцией = (копировать значение);
2. Строки C++ можно сравнивать типовыми операциями: ==, !=, =. Строки
сравниваются в лексографическом порядке. Естественно, что итог сравнения одних и тех же
строк зависит от выбранной локали;
3. Строки можно конкатенировать
соответственно +=);

(объединять)

простым

указанием

операции

+

(и,

4. Существует метод c_str(), возвращающий внутреннее содержимое строки в форме
массива символов (const char*);
Как видно и из последнего утверждения, переменные-объекты класса string/wstring — это
неизменяемые объекты (в том же смысле, как в языке Python и др.). Это не означает константность,
это совсем другое:
string s = "строка 1";
s = "строка 2"

Здесь операцией присвоения переменной s будет присвоен новый объект, созданный вызовом
конструктора с инициализирующим значением "строка 2". Предыдущий объект с значением
"строка 1" будет уничтожен, для него будет вызван деструктор при выходе из области
определения объекта (блока). Новый и старый объекты будут размещены по разным адресам. В
этом смысле и понимается неизменяемость: при модификации значения объекта, новое значение
не изменяет старое, а инициализирует новый объект.
Все эти принципы полностью переносятся и на локализованные строки широких символов
wstring, с той единственной разницей, что string является контейнером однобайтовых char, а
wstring — это контейнер 4-х байтовых широких символов wchar_t.

Потоки ввода-вывода локализованных символов
Библиотеки C++ определяют () отдельные потоки ввода вывода для широких строк,
wcout (эквивалент cout) и wcin (эквивалент cin). Точно так же как и на языке C, прежде чем
осуществлять операции с потоками широких символов, необходимо установить (изменить) локаль
программы:
#include
#include
using namespace std;
void test00(void) {
locale::global(locale(""));
wcout