Несколько дней назад я разместил статью
по алгоритму кодирования UF-8. В сегодняшней статье мы узнаем несколько забавных вещей о UTF-8 и C++. Затем мы приступим к реализации алгоритма кодирования и декодирования UTF-8 на C++.
Обратите внимание, что во многих книгах по программированию для Windows рекомендуется использовать расширенные символы для интернационализированного текста, но я считаю, что использование одиночных байтов, закодированных как UTF-8, в качестве внутреннего представления строк является гораздо более мощным и элегантным подходом. Причина этого в том, что проще использовать символьные функции в стандартных C и C++. Разработчики обычно гораздо лучше знакомы с такими функциями, как strcpy в C или классом std::string C++, чем с эквивалентами расширенных символов wcscpy и std::wstring, а поддержка расширенных символов не полностью согласована ни в одном из стандартов. Например, класс C++ std::exception принимает только описания std::string в своем конструкторе. Кроме того, использование типов char и std::string делает код гораздо более переносимым между платформами, поскольку тип char всегда по определению имеет длину в один байт, тогда как sizeof(wchar_t)
обычно может быть 2 или 4 в зависимости от платформы.
Даже если мы разрабатываем приложение только для Windows, рекомендуется максимально изолировать части, зависящие от Windows, а использование строк в кодировке UTF-8 — надежный способ обеспечить полную поддержку Unicode с очень портативный и читаемый стиль кодирования.
Но, конечно, есть ряд предостережений, которые необходимо учитывать при использовании UTF-8 в строках C и C++. Я резюмировал свои рекомендации по кодированию строк в пяти правилах, которые я перечисляю ниже. Я надеюсь, что другие найдут этот подход полезным. Не стесняйтесь использовать раздел комментариев для любой критики или предложений.
- 1. Первое правило: Используйте UTF-8 в качестве внутреннего представления текста.
- 2. Второе правило: в Visual Studio избегайте любых символов, отличных от ASCII, в исходном коде.
- 3. Третье правило: переводить между UTF-8 и UTF-16 при вызове функций Win32.
- 4. Четвертое правило: используйте расширенные версии стандартных функций C и C++, которые принимают пути к файлам.
- 5. Пятое правило: Будьте осторожны со сторонними библиотеками.
- 6. Ссылки
- Декодировать/Кодировать
- Декодирование строки UTF-8
- Настройка Windows 10/11
- Юникод
- Изображение персонажа
- Строковый литерал C++
- Кодирование источника и выполнения
- Кодировка исполнения по умолчанию
- Кодовые точки Unicode в литерале
- std::string и UTF-8
- Явное кодирование выполнения
- Здесь нечего (или очень мало) делать
- Длина кодирования
- Кодировка в UTF-8
- Кодировка
- Теперь все вместе
- Следующие шаги
- Визуализация кодировки UTF-8
- Полезные свойства
- Подсчет кодовых точек в строке
- Загрузка кодировки
- Подтвердить!
- Проверка
- Получить персонажей
- Заключение
- Заключение
1. Первое правило: Используйте UTF-8 в качестве внутреннего представления текста.
Исходным типом символов в языке C является char, который удваивается как тип, представляющий одиночные байты, атомарный размер памяти, к которой может обращаться программа C и который обычно состоит из восьми битов ( «октет»). Программисты на C привыкли рассматривать текст как массив символов, а стандартная библиотека C предлагает набор функций для обработки текста, таких как strcmp и strcat, с которыми должен быть полностью знаком любой уважающий себя программист на C. Стандартная библиотека C++ предлагает класс std::string, который инкапсулирует массив символов в стиле C, и все программисты на C++ знакомы с этим классом. Поскольку использование char и std::string — это оригинальный и естественный способ кодирования текста в языках C и C++, я думаю, что лучше придерживаться этих типов и избегать расширенных типов символов.
Почему широких символов лучше избегать? Что ж, поддержка расширенных символов в C и C++ была несколько ошибочной и непоследовательной. Они были добавлены как в C, так и в C++ в то время, когда считалось, что 16 битов будет достаточно для хранения любого символа Unicode, что уже не так. Стандартные спецификации C99 и C++98 включают тип wchar_t (как стандартное определение типа в C и как встроенный тип в C++), а текстовые литералы, которым предшествует оператор L, например L»hello», обрабатываются как массив символов wchar_t. Для обработки расширенных символов стандартные функции C, такие как strcpy и strcmp, были воспроизведены в версиях для расширенных символов (wcscpy, wcscmp и т. д.), которые также являются частью стандартных спецификаций, а стандартная библиотека C++ определяет свой строковый класс. как шаблон std::basic_string с параметризованным символьным типом, который допускает существование двух специализаций std::string и std::wstring. Но использование широких символов не может полностью заменить старые простые символы. На самом деле все еще существует ряд стандартных функций и классов, которым требуются строки на основе символов, а использование расширенных символов часто зависит от специфичных для компилятора расширений, таких как _wfopen. Еще один серьезный недостаток заключается в том, что тип wchar_t не имеет фиксированного размера в разных компиляторах, поскольку стандартные спецификации как в C, так и в C++ не предписывают какой-либо размер или кодировку. Это может привести к проблемам при сериализации и десериализации текста на разных платформах. В Visual Studio sizeof(wchar_t)
возвращает 2, что означает, что он может хранить только текст, представленный в кодировке UTF-16, как это делает Windows по умолчанию. U TF-16 представляет собой кодирование переменной ширины
, как UTF-8. Я упомянул в моем предыдущем посте
как было время в прошлом, когда считалось, что 16 бит будет достаточно для всего репертуара символов Unicode, но от этой идеи давно отказались, и 16-битные символы могут использоваться для поддержки Unicode только тогда, когда кто-то принимает что существуют «суррогатные пары», то есть символы (в смысле кодовых точек Unicode), которые в своем представлении охватывают пару 16-битных единиц. Таким образом, идея о том, что каждый wchar_t будет полным символом Unicode, больше не соответствует действительности, и даже при использовании строк на основе wchar_t мы можем встретить фрагменты полных символов.
Проблема переменного размера единиц wchar_t в разных компиляторах была решена в будущих стандартах C и C++ ( C1X
и С++0x
), которые определяют новые типы расширенных символов char16_t и char32_t. Эти новые типы, как видно из их названий, имеют фиксированный размер. Благодаря новым типам строковые литералы теперь могут иметь пять (!) разновидностей:
char plainString[] = «привет»; //Локальная кодировка, какой бы она ни была. wchar_t wideString[] = L"привет"; // Широкие символы, обычно UTF-16 или UTF-32. char utf8String[] = u8 «привет»; //Кодировка UTF-8. char16_t utf16String[] = u"привет"; //Кодировка UTF-16. char32_t utf32String[] = U"привет"; //Кодировка UTF-32.
Итак, в C1x и C++0x у нас теперь есть пять различных способов объявления строковых литералов для учета различных кодировок и четырех различных типов символов. C++0x также определяет типы std::u16string и std::u32string на основе шаблона std::basic_string, точно так же, как std::string и std::wstring. Однако поддержка этих типов по-прежнему неудовлетворительна, и любой код, который полагается на эти типы, в конечном итоге будет управлять большим количеством преобразований кодировки при работе со сторонними библиотеками и устаревшим кодом. Из-за этих ограничений широких типов символов я думаю, что гораздо лучше придерживаться старых простых символов как лучшего представления текста в языках C и C++.
Обратите внимание, что вопреки широко распространенному заблуждению вполне возможно использовать строки на основе символов для текста в C и C++ и полностью поддерживать Unicode. Нам просто нужно убедиться, что весь текст закодирован как UTF-8, а не как узкий набор символов, зависящий от региона, такой как кодировка ISO-8859-1, используемая для некоторых западноевропейских языков. Поддержка Unicode зависит от того, какую кодировку мы применяем для текста, а не от того, выбираем ли мы одиночные байты или широкие символы в качестве атомарного типа символов.
Очевидным недостатком использования символов и UTF-8 для поддержки Unicode является отсутствие взаимно однозначного соответствия между символами как атомарными единицами строки и кодовыми точками Unicode, которые представляют полную символы, отображаемые шрифтами. Например, в строке в кодировке UTF-8 буква с акцентом, такая как «á», состоит из двух символов, тогда как китайский символ, такой как «中», состоит из трех символов. Из-за этого строку в кодировке UTF-8 можно рассматривать как прославленный массив байтов, а не как реальную последовательность символов. Однако это редко является проблемой, потому что мало ситуаций, когда границы между кодовыми точками Unicode вообще имеют значение. При использовании UTF-8 нам не нужно повторять кодовую точку за кодовой точкой, чтобы найти определенный символ или фрагмент текста в строке. Это связано с блестящей гарантией UTF-8, что двоичное представление одного символа не может быть частью двоичного представления другого символа. И когда мы имеем дело с размерами строк, нам может понадобиться знать, сколько байтов составляет строка для целей распределения, но размер с точки зрения кодовых точек Unicode совершенно бесполезен в любом реалистичном сценарии. Кроме того, понятие того, что представляет собой один отдельный персонаж, чревато серыми областями. Существует множество шрифтов ( арабский
и Деванагари
, например), где буквы могут быть объединены вместе в лигатурных формах, и может быть немного спорным вопрос, следует ли рассматривать такие лигатуры как один символ или последовательность отдельных символов. В тех немногих случаях, когда нам может потребоваться перебирать кодовые точки Unicode, например, алгоритм переноса слов, мы можем сделать это с помощью служебной функции или класса. Написать такой класс на C++ несложно, но я оставлю это для следующего поста.
2. Второе правило: в Visual Studio избегайте любых символов, отличных от ASCII, в исходном коде.
const char kChineseSampleText[] = "中文";
Visual Studio предупредит меня при попытке сохранить файл о том, что текущая кодировка не поддерживает все символы, встречающиеся в документе, и предложит мне сохранить его в другой кодировке, например UTF- 8 или UTF-16 (которую Visual Studio, как и другие программы Microsoft, небрежно называет «Unicode»). Даже если мы сохраним исходный файл как UTF-8, Visual Studio сохранит файл с правильными символами, но компилятор выдаст предупреждение:
"warning C4566: character represented by universal-character-name '\u4E2D' cannot be represented in the current code page (1252)"
Это означает, что даже если мы сохраним файл как UTF-8, компилятор Visual Studio все равно попытается интерпретировать литералы как имеющие текущую кодовую страницу (узкий набор символов) системы. Ситуация улучшится с выходом стандарта C++0x. В более новом стандарте, лишь частично поддерживаемом текущими компиляторами, можно будет написать:
const char kChineseSampleText[] = u8"中文";
И префикс u8 указывает, что литерал закодирован как UTF-8. Аналогичные префиксы: «u» для текста UTF-16 в виде массива символов char16_t и «U» для текста UTF-32 в виде массива символов char32_t. Эти префиксы присоединятся к доступному в настоящее время «L», используемому для строк на основе wchar_t. Но пока эта функция не станет доступной в основных компиляторах, лучше ее игнорировать.
Из-за этих ограничений при использовании текста UTF-8 в исходных файлах я полностью избегаю использования любых символов, отличных от ASCII, в моем исходном коде. Для меня это не сложно, потому что все комментарии я пишу на английском языке. Программистам, которые пишут комментарии на другом языке, может быть полезно сохранить файлы в кодировке UTF-8, но даже это будет проблемой для Visual Studio и не обязательно, если язык, используемый для комментариев, является частью узкого набора символов. системы. К сожалению, нет способа указать Visual Studio, по крайней мере в версии Express, сохранять все новые файлы в кодировке Unicode, поэтому нам придется использовать внешний редактор, такой как Notepad++, чтобы пересохранить все исходные файлы в кодировке UTF-8. со спецификацией, чтобы Visual Studio с этого момента распознавала и учитывала кодировку UTF-8. Но было бы мучительно зависеть либо от сохранения файлов с помощью внешнего редактора, либо от набора текста на китайском языке, чтобы заставить Visual Studio спросить нас об используемой кодировке, поэтому я предпочитаю придерживаться только символов ASCII в своем исходном коде. Таким образом, исходные файлы представляют собой обычный ASCII, и мне не нужно беспокоиться о проблемах с кодировкой.
Обратите внимание, что в моей английской версии Visual Studio предупредит меня о том, что китайские символы выходят за пределы узкого набора символов, установленного по умолчанию, но не предупредит меня, если я напишу:
const char kSpanishSampleText[] = "cañón";
Это потому, что «cañón» можно представить в местной кодовой странице. Но приведенная выше строка будет тогда закодирована как ISO-8859-1, а не UTF-8, поэтому такой жестко закодированный текст нарушит правило использования UTF-8 в качестве внутреннего представления текста, и необходимо соблюдать осторожность, чтобы избежать такие жестко запрограммированные литералы. Конечно, если вы используете версию Visual Studio на китайском языке, вам следует поменять местами примеры в моем объяснении. В китайской системе «cañón» будет выдавать предупреждения, тогда как «中文» будет принят компилятором, но будет сохранен в китайской кодировке, такой как GB или Big5.
Пока Microsoft не добавит поддержку префикса ‘u8’ и лучшие параметры для применения UTF-8 в качестве кодировки по умолчанию для исходных файлов, единственный способ жесткого кодирования строковых литералов, отличных от ASCII, в UTF- 8 состоит в перечислении числовых значений для каждого байта. Это то, что я делаю, когда хочу проверить поддержку интернационализации и согласованность при использовании UTF-8. Обычно я использую следующие три строки:
// Китайские иероглифы для «чжунвэнь» («китайский язык»).
const char kChineseSampleText[] = {-28, -72, -83, -26, -106, -121, 0};
// арабский "аль-арабийа" ("арабский").
const char kArabicSampleText[] = {-40, -89, -39, -124, -40, -71, -40, -79, -40, -88, -39, -118, -40, -87, 0 };
// Испанское слово «канон» с буквой «н» с «~» сверху и буквой «о» с ударением.
const char kSpanishSampleText[] = {99, 97, -61, -79, -61, -77, 110, 0}; Эти жестко закодированные строки полезны для тестирования. Первое — китайское слово «中文», второе — арабское «العربية», полезное для проверки правильности отображения текста справа налево, а третье — испанское слово «cañón». Обратите внимание, как я избегал символов, отличных от ASCII, в комментариях. Конечно, в окончательном приложении текст в кодировке UTF-8, отображаемый в графическом интерфейсе, должен загружаться из файлов ресурсов, но приведенный выше жестко закодированный текст пригодится для быстрых тестов.
Использование UTF-8 в качестве нашего внутреннего представления текста имеет еще одно важное последствие при использовании Visual Studio, заключающееся в том, что текст не будет правильно отображаться в окнах просмотра, которые показывают значения переменных во время отладки. Это связано с тем, что эти окна отладки также предполагают, что текст закодирован в локальном узком наборе символов. Вот как три приведенных выше константных литерала выглядят в моей версии Visual Studio:

К счастью, есть способ отобразить правильные значения благодаря спецификатору формата ‘s8’
. Если мы добавим ‘,s8’ к именам переменных, Visual Studio повторно проанализирует текст в кодировке UTF-8 и правильно отобразит текст:

Спецификатор формата «s8» работает с массивами простых символов, но не с переменными std::string. Я сообщил об этом
в прошлом году Microsoft, но они, по-видимому, отложили исправление до будущей версии Visual Studio.
3. Третье правило: переводить между UTF-8 и UTF-16 при вызове функций Win32.
При программировании для Windows нам необходимо понимать, как работает Win32 API и другие библиотеки, построенные на его основе, такие как MFC, обрабатывают символы и Unicode. До Windows NT предыдущие версии операционной системы, такие как Windows 95, не поддерживали Unicode внутри, а поддерживали только узкий набор символов, который зависел от языка системы. Так, например, англоязычная версия Windows 95 будет поддерживать символы западноевропейской кодовой страницы 1252, которая почти совпадает с ISO-8859-1. Из-за предположения, что все эти региональные кодировки подпадают под действие того или иного стандарта, эти наборы символов называются «ANSI» (Американский национальный институт стандартов) в устаревшей терминологии Microsoft. В те времена, когда еще не было Юникода, функции и структуры Win32 API использовали в качестве параметров простые строки на основе символов. С появлением Windows NT Microsoft приняла решение о внутренней поддержке Unicode, чтобы файловая система могла работать с именами файлов, используя любые произвольные символы, а не узкий набор символов, меняющийся от региона к региону. Оглядываясь назад, я думаю, что Microsoft должна была выбрать поддержку кодировки UTF-8 и придерживаться простых символов, но в то время они разделили функции и структуры, использующие строки, на две альтернативные разновидности, одна из которых использует простые символы. и еще один, который использует wchar_t. Например, старая функция MessageBox была разделена на две функции, так называемая версия ANSI:
int MessageBoxA(HWND hWnd, const char* lpText, const char* lpCaption, UINT uType);
и Unicode:
int MessageBoxW(HWND hWnd, const wchar_t* lpText, const wchar_t* lpCaption, UINT uType);
Причина этих уродливых имен, оканчивающихся на «A» и «W», в том, что Microsoft не ожидала, что эти функции будут использоваться напрямую, а через макрос:
#ifdef ЮНИКОД #define MessageBox MessageBoxW #еще #define MessageBox MessageBoxA #endif // !ЮНИКОД
int MessageBox(HWND hWnd, const TCHAR* lpText, const TCHAR* lpCaption, UINT uType);
Именно так документируются функции Win32 API (за исключением того, что Microsoft использует уродливое определение типа LPTSTR для const TCHAR*). Благодаря этому синтаксическому сахару Microsoft обращается с макросами так, как если бы они были реальными функциями, а тип TCHAR — как с отдельным символьным типом. Макрос TEXT вместе с его более коротким вариантом _T также #определен таким образом, что можно предположить, что литерал типа TEXT(«hello») или _T(«hello») имеет тип const TCHAR* и будет компилироваться. независимо от того, является ли UNICODE #defined или нет. Этот изящный механизм позволяет писать код, в котором все символы будут скомпилированы либо в простые символы в узком наборе символов, либо в широкие символы в кодировке Unicode (UTF-16). То, как символы разрешаются во время компиляции, просто зависит от того, был ли макрос UNICODE определен глобально для всего проекта. Тот же механизм используется для библиотеки времени выполнения C, где такие функции, как strcmp и wcscmp, могут быть заменены макросом _tcscmp, а поведение таких макросов зависит от того, был ли #defined ‘_UNICODE’ (обратите внимание на подчеркивание). Макросы «UNICODE» и «_UNICODE» можно включить для всего проекта, установив флажок в диалоговом окне «Настройки проекта».
Теперь этот способ написания кода, который может быть скомпилирован либо как ANSI, либо как Unicode с помощью переключателя глобальной настройки, возможно, имел смысл в то время, когда поддержка Unicode не была универсальной в Windows. Но версии Windows, которые все еще поддерживаются сегодня (XP, Vista и 7), являются самыми последними выпусками Windows NT, и все они внутри используют Unicode. Из-за этого возможность компилировать альтернативные версии ANSI и Unicode в настоящее время не имеет никакого смысла, и для ясности лучше вообще избегать типа TCHAR и использовать явный тип символа при вызове функций Windows.
std::string ConvertFromUtf16ToUtf8(const std::wstring& wstr)
{
std::string convertString;
int requiredSize = WideCharToMultiByte (CP_UTF8, 0, wstr.c_str(), -1, 0, 0, 0, 0);
если (требуемый размер > 0)
{
std::vector buffer(requiredSize);
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &buffer[0], requiredSize, 0, 0);
convertString.assign(buffer.begin(), buffer.end() - 1);
}
вернуть преобразованную строку;
}
std::wstring ConvertFromUtf8ToUtf16 (const std::string& str)
{
std::wstring convertString;
int requiredSize = MultiByteToWideChar (CP_UTF8, 0, str.c_str(), -1, 0, 0);
если (требуемый размер > 0)
{
std::vector буфер (требуемый размер);
MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &buffer[0], requiredSize);
convertString.assign(buffer.begin(), buffer.end() - 1);
}
вернуть преобразованную строку;
} Изменить 27 июля 2014 г.: в С++ 11 вышеуказанные функции могут быть реализованы переносимым способом, путем определения переменной типа std::wstring_convert <std::codecvt_utf8_utf16 <Utf16Char>, Utf16Char>
и зов to_bytes
и from_bytes
для преобразования UTF-8 в UTF-16 и UTF-16 в UTF-8 соответственно. Visual Studio 2012 поддерживает это.
void SetWindowTextUtf8(HWND hWnd, const std::string& str)
{
std::wstring wstr = ConvertUtf8ToUtf16(str);
SetWindowTextW(hWnd, wstr.c_str());
}
std::string GetWindowTextUtf8(HWND hWnd)
{
std::строка ул;
int requiredSize = GetWindowTextLength(hWnd) + 1; // Мы должны учитывать последний нулевой символ.
если (требуемый размер > 0)
{
std::vector буфер (требуемый размер);
GetWindowTextW(hWnd, &buffer[0], requiredSize);
std::wstring wstr(buffer.begin(), buffer.end() - 1);
ул = ConvertUtf16ToUtf8 (wstr);
}
вернуть ул;
} Две приведенные выше функции упрощают чтение и запись текста из объектов HWND. Во-первых, они предоставляют полезный фасад C++ .
что скрывает грязную работу по работе с размерами буфера. Что еще более важно, они скрывают детали использования расширенных символов, так что клиентский код может предположить, что читаемые и записываемые строки std::strings закодированы как UTF-8.
4. Четвертое правило: используйте расширенные версии стандартных функций C и C++, которые принимают пути к файлам.
Существует очень неприятная проблема с реализацией Microsoft стандартных библиотек C и C++. Представьте, что мы храним путь поиска для некоторых файловых ресурсов в качестве переменной-члена в одном из наших классов. Поскольку мы всегда используем UTF-8 для нашего внутреннего представления, этот элемент будет std::string в кодировке UTF-8. Теперь мы можем захотеть открыть или создать временный файл в этом каталоге, добавив имя файла к пути поиска и вызвав функцию времени выполнения C fopen. В C++ мы бы предпочли использовать объект std::fstream, но в любом случае мы обнаружим, что все работает нормально, если полный путь к имени файла содержит только символы ASCII. Проблема может оставаться незамеченной до тех пор, пока мы не попробуем путь, содержащий букву с диакритическим знаком или китайский символ. В этот момент код, использующий fopen или std::fstream, перестанет работать. Причина этого в том, что стандартные неширокие функции и классы, работающие с файловой системой, такие как fopen и std::fstream, предполагают, что символьная строка закодирована с использованием кодовой страницы локальной системы, и нет способ заставить их работать со строками UTF-8. Обратите внимание, что на данный момент (2011 г.) Microsoft не позволяет устанавливать локаль UTF-8 с помощью функции set_locale C. Согласно MSDN:
Набор доступных языков, кодов страны/региона и кодовых страниц включает все те, которые поддерживаются Win32 NLS API, за исключением кодовых страниц, для которых требуется более двух байтов на символ, таких как UTF-7 и UTF-8. Если вы укажете кодовую страницу, такую как UTF-7 или UTF-8, setlocale завершится ошибкой и вернет NULL. Набор кодов языка и страны/региона, поддерживаемых setlocale, указан в Language and Country/Region Strings
( источник
).
Это очень прискорбно, потому что код, использующий fopen или std::fstream, является стандартным C++, и мы ожидаем, что этот код станет частью нашего полностью переносимого кода, который можно будет скомпилировать под Windows, Mac или Linux без каких-либо изменений. Однако из-за невозможности настроить реализацию Microsoft стандартных библиотек для использования кодировки UTF-8, мы должны предоставить обертки для этих функций и классов, чтобы строка UTF-8 преобразовывалась в широкую строку UTF-16, а вместо этого внутри используются функции и классы, использующие расширенные символы, такие как _wfopen и std::wfstream. Написание оболочки для std::fstream до Visual C++ 10 было затруднено, потому что только широкосимвольные объекты файлового потока wifstream и wofstream могли быть созданы с широкими символами, но использование таких объектов для извлечения текста было проблемой, поэтому они были почти бесполезны, если только дополненный собственными гранями перекодировки (достаточно много работы). С другой стороны, было невозможно получить доступ к файлу с именем, включающим символы вне локальной кодовой страницы (например, «中文.txt» в испанской системе), используя основанные на символах ifstream и ofstream. К счастью, последняя версия компилятора C++ расширила конструкторы стандартных объектов fstream для поддержки расширенных типов символов ( в соответствии с новыми дополнениями к готовящемуся стандарту C++0x
; Редактировать 27 июля 2014 г.: это неверно. Как упомянул Сет в комментарии ниже, в С++ 11 вообще не добавлены конструкторы широких символов .
), что упрощает задачу написания таких оберток:
пространство имен utf8
{
#ifdef WIN32
класс ifstream: общедоступный std::ifstream
{
публичный:
еслипоток() : std::ifstream() {}
явный ifstream(const char* fileName, std::ios_base::open_mode mode = std::ios_base::in) :
std::ifstream(ConvertFromUtf8ToUtf16(fileName), режим)
{
}
явный ifstream(const std::string& fileName, std::ios_base::open_mode mode = std::ios_base::in) :
std::ifstream(ConvertFromUtf8ToUtf16(fileName), режим)
{
}
void open(const char* fileName, std::ios_base::open_mode mode = std::ios_base::in)
{
std::ifstream::open(ConvertFromUtf8ToUtf16(fileName), режим);
}
void open(const std::string& fileName, std::ios_base::open_mode mode = std::ios_base::in)
{
std::ifstream::open(ConvertFromUtf8ToUtf16(fileName), режим);
}
};
класс потока: общедоступный std::ofstream
{
публичный:
ofstream() : std::ofstream() {}
явный поток (const char* имя_файла, std::ios_base::open_mode mode = std::ios_base::out) :
std::ofstream(ConvertFromUtf8ToUtf16(fileName), режим)
{
}
явный поток (const std::string& fileName, std::ios_base::open_mode mode = std::ios_base::out) :
std::ofstream(ConvertFromUtf8ToUtf16(fileName), режим)
{
}
void open(const char* fileName, std::ios_base::open_mode mode = std::ios_base::out)
{
std::ofstream::open(ConvertFromUtf8ToUtf16(fileName), режим);
}
void open(const std::string& fileName, std::ios_base::open_mode mode = std::ios_base::out)
{
std::ofstream::open(ConvertFromUtf8ToUtf16(fileName), режим);
}
};
#еще
typedef std::ifstream ifstream;
typedef std::ofstream ofstream;
#endif
} // пространство имен utf8 Благодаря этому коду, с минимальным изменением замены utf8::ifstream и utf8::ofstream для стандартных версий, мы можем использовать пути к файлам в кодировке UTF-8 для открытия файловых потоков. На C аналогичная оболочка может быть написана для функции fopen.
5. Пятое правило: Будьте осторожны со сторонними библиотеками.
6. Ссылки
Декодировать/Кодировать
Декодирование и кодирование символов — это всего лишь вопрос маскирования интересующих битов и сдвига их в целое число.
Чтобы понять, что происходит в этих функциях, держите перед собой таблицу со структурой кодировки UTF-8.
// from UTF-8 encoding to Unicode Codepoint // From Unicode Codepoint to UTF-8 encoding Декодирование строки UTF-8
С помощью этой функции мы можем декодировать UTF-8.
/* * Каждая кодовая точка декодируется, а затем добавляется к * результирующий вектор. Если строка имеет неверную кодировку * то процесс останавливается и возвращается false. */
bool utf8_decode(const std::string& str, std::vector& результат) { если (ул.пусто()) { //Нечего делать вернуть истину; } //Для байтов без знака используйте uint8_t, а не char. // char может быть подписан на некоторых платформах. С использованием // char действительно может привести к дефектам. uint8_t bytes[] = {0, 0, 0, 0}; for (size_t idx = 0; idx < str.length(); ++idx) { bytes[0] = str.at(idx); если ((байты [0] & 0b10000000) == 0) { результат.push_back (байты [0]); } иначе если ((bytes[0] & 0b11100000) == 0b11000000) { если (idx + 1 == str.length()) { //Ошибка. Недостаточно байтов. вернуть ложь; } bytes[1] = str.at(++idx); если ((байты[1] и 0b11000000) != 0b10000000) { //Ошибка. Не последующий байт. вернуть ложь; } //Очистить биты маркера UTF-8 байты[0] = байты[0] & 0b00011111; байты[1] = байты[1] & 0b00111111; длинный код без знака = (bytes[0] << 6) | байты[1]; результат.push_back(код); } иначе если ((bytes[0] & 0b11110000) == 0b11100000) { если (idx + 2 == str.length()) { //Ошибка. Недостаточно байтов. вернуть ложь; } bytes[1] = str.at(++idx); bytes[2] = str.at(++idx); если ((байты[1] и 0b11000000) != 0b10000000) { //Ошибка. Не последующий байт. вернуть ложь; } если ((байты[2] и 0b11000000) != 0b10000000) { //Ошибка. Не последующий байт. вернуть ложь; } //Очистить биты маркера UTF-8 байты[0] = байты[0] & 0b00001111; байты[1] = байты[1] & 0b00111111; байты[2] = байты[2] & 0b00111111; длинный код без знака = (bytes[0] << 12) | (байты[1] << 6) | байты[2]; результат.push_back(код); } иначе если ((bytes[0] & 0b11111000) == 0b11110000) { если (idx + 3 == str.length()) { //Ошибка. Недостаточно байтов. вернуть ложь; } bytes[1] = str.at(++idx); bytes[2] = str.at(++idx); bytes[3] = str.at(++idx); если ((байты[1] и 0b11000000) != 0b10000000) { //Ошибка. Не последующий байт. вернуть ложь; } если ((байты[2] и 0b11000000) != 0b10000000) { //Ошибка. Не последующий байт. вернуть ложь; } если ((байты[3] и 0b11000000) != 0b10000000) { //Ошибка. Не последующий байт. вернуть ложь; } //Очистить биты маркера UTF-8 байты[0] = байты[0] & 0b00000111; байты[1] = байты[1] & 0b00111111; байты[2] = байты[2] & 0b00111111; байты[3] = байты[3] & 0b00111111; длинный код без знака = (bytes[0] << 18) | (байты[1] << 12) | (байты[2] << 6) | байты[3]; результат.push_back(код); } еще { //Мы не можем декодировать этот байт вернуть ложь; } } вернуть истину;
} std::string str = "A\u03A9\u8A9E\U0001F60D";
std::vector результат;
если (utf8_decode (строка, результат)) { std::cout << std::hex << результат[0] << ", " << результат[1] << ", " << результат[2] << ", " << результат[3] << стд::эндл;
} еще { std::cout << "Недопустимая строка UTf-8. Ошибка декодирования." << стд::эндл;
} Настройка Windows 10/11
Windows 10/11 плохо отображает строки в кодировке UTF-8. Выполните следующие действия, чтобы включить UTF-8 в командной строке и терминале.
- Бежать
intl.cpl
команда, чтобы открыть международную настройку. - Щелкните Административный
вкладка - Нажмите Изменить язык системы
. - Установите флажок Бета-версия: используйте Unicode UTF-8 для поддержки языков во всем мире
. - Перезагрузка.
Терминал Windows более современен, чем командная строка, и лучше отображает кодовые точки Unicode. Чтобы Visual Studio использовала Windows Terminal для запуска вашей программы, выполните следующие действия.
- Откройте настройки Windows.
- Нажмите Конфиденциальность и безопасность
. - Установите терминал по умолчанию на Терминал Windows.
Юникод
Сегодня мир говорит на Unicode. Точнее, сегодня Мир говорит в кодировке UTF-8 Unicode. Итак, до ISO-8859-x, KOI8 и Shift-JIS; а также в UCS-2 и другие многобайтовые кодировки!
Мы искренне приветствуем сохранение 7-битного ASCII, но давайте сделаем все возможное, чтобы способствовать повсеместному использованию UTF-8. Это стоит того!
Для нас, программистов на C, цена, которую мы должны заплатить, — избавиться от char
шрифт, который мы все знаем и любим; с предположением, что символ поместится в байт, это уже не подходит.
Представлен C90 wchar_t
и множество функций, помогающих работать с кодировками, отличными от ASCII, но я всегда находил их излишне сложными и запутанными.
Конечно, при работе со строками Unicode вы можете использовать одну из библиотек Unicode, доступных для C (например, ICU
и либунистинг
) и используйте его, но они очень сложны и, возможно, вам не нужны все функции, которые они предлагают.
Благодаря красоте UTF-8 (чистый гений), скорее всего, вам потребуется очень мало кода или вообще не потребуется кода для обработки строк в кодировке UTF-8 в зависимости от того, что вы должны делать с этими строками.
Давайте углубимся, чтобы понять, когда нужна полноценная библиотека, а когда можно просто использовать уже имеющиеся в вашем распоряжении инструменты.
Изображение персонажа
Для внутреннего представления персонажей у нас есть два варианта:
- декодировать кодовую точку и использовать значение Unicode в нашей программе
- напрямую использовать закодированную форму
Первое кажется более логичным
но если подумать, часто это пустая трата времени. Рассмотрим, например, сравнение двух строк. Сравнение закодированной формы точно такое же, как сравнение значений кодовой точки, но при этом сохраняется шаг декодирования для двух строк.
Однако я утверждаю, что ссылаясь на «ジ» как на 30B8
(кодовая точка Unicode) или как E382B8
(его кодировка UTF-8) не имеет большого значения.
Любая допустимая кодировка UTF-8 умещается в 4 байта, поэтому давайте определим новый тип u8chr_t
:
Строковый литерал C++
Кодирование источника и выполнения
Исходная кодировка определяет, как был закодирован файл исходного кода.
Кодировка выполнения определяет, как строковый литерал кодируется и сохраняется в исполняемом файле. Это также кодирование строки в памяти во время выполнения. Кодировка выполнения зависит от компилятора.
std::cout << "I \U0001F60D \u03A9" << std::endl;
Я 😍 Ω
Кодировка исполнения по умолчанию
Рассмотрим простой строковый литерал.
const char *s = "Hello World";
Какова кодировка исполнения этой строки? Короткий ответ заключается в том, что он зависит от компилятора. L LVM всегда делает строку в кодировке UTF-8. M SVC также пытается кодировать литералы с использованием UTF-8. Но на это могут повлиять несколько факторов
.
Кодовые точки Unicode в литерале
Кодовая точка — это просто целое число без знака, присвоенное символу. Например, символу 😍 присвоено значение 0x0001F60D
. Кодовая точка часто выражается с использованием синтаксиса U+0001F60D
.
Схема кодирования, такая как UTF-8 или UTF-16, применяет определенные алгоритмы для кодирования значений кодовой точки в байты.
Если вы можете сохранить исходный код в кодировке UTF-8, а компилятор использует кодировку выполнения UTF-8, то вы можете напрямую указать кодовые точки в кодировке UTF-8 в строке.
//Source code saved as a UTF-8 encoded file.
std::cout << "I 😍 Ω" << std::endl; Я 😍 Ω
Может быть удобнее указывать кодовые точки, используя их целые значения. Это делается с помощью \u
или \U
побег. \u
escape занимает ровно 4 шестнадцатеричных цифры. \U
escape занимает ровно 8 шестнадцатеричных цифр.
std::cout << "I \U0001F60D \u03A9" << std::endl;
Я 😍 Ω
При использовании escape-последовательности исходный код больше не нужно сохранять в файле с кодировкой UTF-8. Например, это может быть обычный ASCII. Компилятор преобразует литерал в кодировку UTF-8 и сохраняет его в исполняемом файле.
std::cout << "\u{0001F60D}" << std::endl; std::string и UTF-8
std::string
class ничего не знает о кодировке UTF-8. Он работает просто с последовательностью байтов. Например, length()
Метод возвращает количество байтов, а не количество кодовых точек.
std::string s("I \U0001F60D \u03A9");
std::cout << s.length() << std::endl;
//Выводит 9 Это напечатает 9, потому что после кодирования UTF-8 U+0001F60D занимает 4 байта, а U+03A9 занимает 2 байта.
Явное кодирование выполнения
const char8_t *s = u8"I \U0001F60D \u03A9";
К сожалению, char8_t
имеет плохую поддержку существующей кодовой базы. Вы даже не можете напечатать строку, используя cout
.
std::cout << u8"I \U0001F60D \u03A9" << std::endl; // Печатает адрес указателя. Пример: 0x100003cc3
Схема памяти const char8_t *
литерал идентичен a const char *
буквальный. Но эти два типа несовместимы. Например, вы не можете построить std::string
из const char8_t *
.
// Не компилируется std::string s(u8"I \U0001F60D \u03A9");
std::u8string
был представлен для работы с const char8_t *
.
//Компилируется нормально std::u8string s(u8"I \U0001F60D \u03A9");
К сожалению, std::u8string
не совместим с std::string
и std::u8string
имеет плохую поддержку существующей кодовой базы. cout
не могу печатать std::u8string
или. std::u8string
не приносит ничего нового на стол. Например, он не может проверить кодировку UTF-8 или вычислить количество кодовых точек.
Люди просто устали от этой ситуации и начали кастинг const char8_t *
до const char *
.
const char *literal = reinterpret_cast(u8"I \U0001F60D \u03A9"); std::string s (литерал); std::cout << s << std::endl;
Я 😍 Ω
Большинство приложений имеют очень мало жестко запрограммированных литеральных строк. Большинство из них обычно находятся в коде модульного тестирования. Я бы не стал обдумывать ситуацию. Просто используйте обычные литералы. Большинство компиляторов будут кодировать их в UTF-8 для выполнения. Если вы должны использовать u8
буквально, тогда я думаю, кастинг const char8_t *
до const char *
достаточно хорошее решение.
Здесь нечего (или очень мало) делать
В результате вышеупомянутых свойств многие функции стандартной библиотеки C продолжают работать (возможно, с некоторыми оговорками):
-
strcpy()
,strcmp()
,strstr()
,fgets()
, и любая другая функция, использующая терминаторы ASCII (\0
,\n
,\t
, .) совершенно не затрагиваются. -
strtok()
,strspn()
,strchr()
, будет работать до тех пор, пока их другой аргумент находится в пределах диапазона ASCII. - Для
strlen()
,strncpy()
и другие размер ограничен
функции,n
Параметр выражает размер (в байтах) буфера, в котором находится строка, а не количество символов в строке.
В общем, для любой функции, которую вы хотите использовать, спросите себя, имеет ли какое-то значение кодировка символов как UTF-8 или нет, и просто напишите тот минимальный код, который вам может понадобиться.
Вы можете использовать кодировку UTF-8 для написания простых функций, подобных этой:
// Returns the number of characters in an UTF-8 encoded string.// (Does not check for encoding validity) Или что-то более сложное (но все же не такое сложное):
// Avoids truncating multibyte UTF-8 encoding at the end. // Last byte has been overwritten Длина кодирования
Чтобы определить длину кодировки по ее первому байту, мы можем определить массив и макрос, который использует старшие 4 бита байта в качестве индекса в массиве:
// 0 1 2 3 4 5 6 7 8 9 A B C D E F Макрос сообщит нам длину кодировки первого символа.
Вот несколько примеров:
Обратите внимание, что последние два являются недопустимой кодировкой. В первом случае сразу можно сказать, что это случайный байт. Во втором нам нужно будет посмотреть на следующие байты, чтобы признать, что кодировка недействительна.
Кодировка в UTF-8
Здесь мы реализуем алгоритм кодирования. Если вы новичок в этом, вы можете прочитать мой более ранний пост
о том, как работает алгоритм.
/* * Эта функция кодирует кодовую точку и, если она действительна, * добавляется к строке. */
void utf8_encode (std::string& str, длинная кодовая_точка без знака) { если (кодовая_точка <= 0x007F) { char ch = static_cast(code_point); ул.push_back(ch); } иначе если (code_point > 6); ул.push_back(b1); ул.push_back(b2); } иначе если (code_point > 6) & 0b111111); uint8_t b1 = 0b11100000 | (кодовая_точка >> 12); ул.push_back(b1); ул.push_back(b2); ул.push_back(b3); } иначе если (code_point > 6) & 0b111111); uint8_t b2 = 0b10000000 | ((code_point >> 12) & 0b111111); uint8_t b1 = 0b11110000 | (кодовая_точка >> 18); ул.push_back(b1); ул.push_back(b2); ул.push_back(b3); ул.push_back(b4); }
} std::string str = ""; utf8_encode (ул, 0x41); utf8_encode (ул, 0x03A9); utf8_encode (ул, 0x8A9E); utf8_encode (ул, 0x1F60D); std::cout << str << std::endl;
Кодировка
Дополнительные правила для действительных
Кодировка UTF:
- должно быть минимальным
(он должен использовать наименьшее возможное количество байтов) - кодовые точки
U+D800
кU+DFFF
(известные как суррогаты UTF-16
) недействительны и, следовательно, их кодировка недействительна.
Я займусь проверкой кодировки в следующем посте, а пока давайте посмотрим, что позволяет нам делать UTF-8, просто игнорируя тот факт, что строка действительно закодирована в UTF-8!
Теперь все вместе
Теперь мы можем завершить нашу исходную функцию, которая будет возвращать длину следующего символа в строке:
Обратите внимание, что в конце мы добавили еще одну проверку: если символ ‘ \0
‘ длина также равна 0. Таким образом, вы можете добавить возвращаемое значение к текущему указателю в строку и быть уверенным, что вы не выйдете за конец строки.
Следующие шаги
Этот пост посвящен легкому
часть, чтобы не отпугнуть вас, но есть два основных аспекта, которые необходимо обсудить:
- проверка
: как определить, действительно ли последовательность байтов является символом в кодировке UTF-8; - складной
: преобразование символов между их прописными и строчными буквами (если есть). Это очень сложный момент и наиболее важный для сравнения без учета регистра, которое является очень распространенной задачей.
Визуализация кодировки UTF-8
Просмотр фактических байтов строки в кодировке UTF-8 может помочь вам изучить алгоритм кодирования. Функция, подобная приведенной ниже, показывает биты именно так, как они появляются в памяти.
void dump_bits(const void *start, size_t size) { unsigned char *buff = (unsigned char*) start; for (size_t byte_loc = 0; byte_loc = 0; --bit_loc) { printf("%u", (buff[byte_loc] & (1 << bit_loc)) != 0); } ставит(""); } ставит("");
} const char *str = "\U0001F60D"; dump_bits(str, strlen(str));
Это будет напечатано.
11110000 10011111 10011000 10001101
Полезные свойства
Кодировка UTF-8 имеет много полезных свойств. В частности:
- Первые 128 символов занимают всего один байт, и их кодировка одинакова как в ASCII, так и в UTF-8.
- два
старшие значащие биты первого байта многобайтовой кодировки11
(т.е. если(b & 0xC0) == 0xC0
байтb
— первый байт многобайтовой кодировки); - два
старшие значащие биты следующих байтов многобайтовой кодировки равны10
(т.е. если(b & 0xC0) == 0x80
байтb
является частью многобайтовой кодировки); - Нет
NUL
символ (‘\0
‘) вводится как побочный продукт кодирования, а это означает, что наше соглашение о том, что строка завершается 0, является безопасным. - UTF-8 сохраняет порядок: относительный порядок двух закодированных символов совпадает с их незакодированным порядком.
Также обратите внимание, что возможность легко идентифицировать первый байт закодированного символа позволяет легко перейти к следующему или предыдущему символу в строке, начиная с любой точки; даже из байта в середине многобайтовой кодировки. Это очень желательное свойство для кодирования, означающее, что можно быстро выполнить повторную синхронизацию, если что-то пошло не так при декодировании.
Подсчет кодовых точек в строке
Кодовая точка UTF-8 занимает один или несколько байтов. Первый байт всегда имеет следующие шаблоны:
- Старший бит равен 0. Или,
- Два старших бита равны 11.
Мы можем использовать этот шаблон для подсчета количества кодовых точек в строке в кодировке UTF-8.
size_t codeptlen(const char* p) { количество_t = 0; в то время как (*р) { если ((*p & 0b10000000) == 0) { //Старший значащий бит равен 0. ++количество; } иначе если ((*p & 0b11000000) == 0b11000000) { //Два старших бита равны 11. ++количество; } ++р; } количество возвратов;
} std::cout << codeptlen("I \U0001F60D \u03A9") << std::endl;
//Выводит 5 Загрузка кодировки
Теперь, когда у нас есть ожидаемая длина, мы можем загружать кодировку по одному байту за раз ( txt
указатель на строку):
Обратите внимание, что код не читает дальше конца строки, если байтов меньше ожидаемого. Если это произойдет, мы выйдем из цикла и encoding
переменная сохранит то, что было найдено до сих пор.
Подтвердить!
А теперь основная часть: давайте определим функцию для проверки правильности загруженной кодировки:
// Reject UTF-16 surrogates В функции всего пять if
е:
-
[1]
Проверьте значения ASCII. -
[3]
Исключите суррогаты UTF-16. - остальные три смотрят как на значения, так и на битовый шаблон, чтобы убедиться, что кодировка действительна.
Это охватывает диапазон U+0080
— U+07FF
(одиннадцать бит), который представлен двумя байтами: 110xxxxx 10xxxxxx
.
Просто заменив биты, легко увидеть, что кодировка для U+0080
это 0xC280
(1100 0010 1000 0000
) и кодировку для U+07FF
это 0xDFBF
(1101 1111 1011 1111
). Любая кодировка ниже 0xC280
должно быть недопустимым (это исходит из не минимальной кодировки!), любая кодировка выше 0xDFBF
будет дополнительно проверено другим if’s.
Однако не все значения в этом диапазоне допустимы: рассмотрим значение ‘0xCAF3’, которое равно 1100 1010 1111 0011
в двоичном формате; второй байт не в форме 10xx xxxx
.
Маскировка кодировки с помощью «0xE0C0» покажет, являются ли биты такими, какими они должны быть: «0xC080».
Если это кажется запутанным, попробуйте использовать пару кодовых точек. Я уверен, что это станет яснее в кратчайшие сроки.
Все остальные if’ы работают точно так же.
Я написал более подробное описание Stack Overflow
как ответ на себя. Я хотел убедиться, что этот метод верен, прежде чем использовать его, и я использовал коллективный разум SO.
Проверка
любая строка UTF-8 должна быть проверена перед использованием.
Не буду вдаваться в причины, но они очень важны.
Итак, когда в предыдущем посте
Я сказал, что « вы можете обрабатывать строку в кодировке UTF-8, как если бы она не была закодирована
» (т.е. пропустить проверку), я действительно имел в виду, что вы можете сделать это , только если вы уверены на 100%
что строка уже была проверена или будет проверена позже, прежде чем будет использована каким-либо осмысленным образом (включая ее сохранение в БД).
Если вы сомневаетесь, вы должны проверить строку самостоятельно.
Глядя на таблицу в предыдущем посте (или в Википедии
), ясно, что не каждая последовательность байтов является допустимой кодировкой UTF-8.
Например, последовательность байтов C2 41
явно недействителен, поскольку второй байт многобайтовой кодировки UTF-8 должен быть больше 0x7F.
Существует множество возможных способов проверки кодировок UTF-8.
Существует чрезвычайно быстрых параллельных алгоритмов
но наша цель здесь — понять, как работает валидация, и давайте подготовимся к этому.
Получить персонажей
Самая простая операция, которую мы выполняем над последовательностью символов, — получение следующего.
Нам нужна функция, которая принимает строку, проверяет, являются ли первые байты действительной кодировкой UTF-8, и, в случае необходимости, возвращает соответствующий символ плюс количество байтов, занимаемых кодировкой.
Давайте решим, что если мы найдем недопустимую последовательность байтов, мы вернем первую (т.е. длину 1) и установим errno
до EINVAL
чтобы сигнализировать об ошибке.
Заключение
Кодирование, декодирование и проверка могут показаться более сложными, чем они есть на самом деле; некоторые манипуляции с битами могут очень легко помочь. Ничего нового.
Немного сложнее расширить обычные функции на символы. Как мы можем получить функции, эквивалентные isdigit()
или tolower()
?
За исключением самых тривиальных задач нам нужны эти функции и об этом будет тема следующего поста
.
Заключение
Как правило:
Когда вас попросят разобраться со строками в кодировке UTF-8 в C, спросите себя, какой аспект кодировки действительно влияет на вашу работу. Вы можете обнаружить, что кодировка UTF-8
нематериально для работы, которую вы должны сделать!

