Не-ASCII строки
Unicode in C and C++: What You Can Do About It Today
Учитывая всё разнообразие языков, существующих на Земле, текст – это не такой уж и тривиальный способ представления информации. Практически любому программисту приходится иметь дело с текстовыми сообщениями на каком-либо человеческом языке, поэтому следует принимать во внимание следующие проблемы:
- Кодировка текста (Encoding) – сопоставление символов алфавита с числами. Следует иметь в виду, что существует огромное количество кодировок.
- Отображение символов (Display). После того как число интерпретировано как определённый символ, необходимо найти шрифт с этим символом и отобразить его. Следует учесть, что текст может отображаться как слева направо, так и справа налево. Существуют специальные знаки, которые модифицируют предыдущий символ, при этом сами имеют нулевую ширину (непосредственно не отображаются). Для некоторых языков необходимы ячейки отображения символов значительно шире, чем для других языков.
- Ввод (Input). Нажатия клавиш при вводе текста должны быть сопоставлены с определёнными символами. При некоторых способах ввода один символ может порождаться нажатием нескольких клавиш.
- Интернационализация (Internationalization – i18n) – перевод текстовых сообщений программы на разные языки.
- Компьютерная лексикография (Lexicography) – интерпретация структуры слов: поиск символов, подстрок, сортировка, смена регистра букв и т.п.
Наиболее сложная часть этих проблем решена на уровне операционной системы, программных интерфейсов и библиотек. Если выбрана подходящая кодировка, то методы ввода и отображения символов, предалагаемые операционной системой, начинают корректно работать "сами по себе". Задача программиста в этом случае – правильно выбрать кодировку. По сравнению с прочими проблемами, проблема интернационализации не так критична, поскольку обычно не угрожает потерей или разрушением данных пользователя. А вот прозрачность имеющихся лексикографических методов сильно зависит от выбранной кодировки.
Долгое время в ходу были 8-битные кодировки, дополняющие 128 символов ASCII символами национальных алфавитов. Примеры таких кодировок, использующихся для символов кириллицы: ISO-8859-5, KOI8-R (ГОСТ 19768-87), CP866 (OEM), Windows-1251 (ANSI). Использование этих кодировок позволяет хранить строки в обычных байтовых массивах (char*), при этом одному символу соответствует один байт. Большинство обычных лексикографических функций из string.h адекватно воспринимают такие строки (однако может неправильно работать сортировка или смена регистра).
К сожалению, использование 8-битных кодировок практически делает невозможным работу программы одновременно с несколькими языками. Поэтому такие кодировки теряют популярность.
В настоящее время универсальной кодировкой, подходящей для работы со всеми языками, считается кодировка Unicode (ISO/IEC 10646). В ней предусмотрено 1112064 кодов для символов.
Для хранения кодов символов этой кодировки достаточно 32 бит на символ. Такой способ представления Unicode называется UTF-32 или UCS-4. Главное преимущество UTF-32 перед формами Unicode переменной длины заключается в том, что символы Unicode непосредственно индексируемы. Получение n-ой кодовой позиции является операцией, занимающей одинаковое время. Это делает замену символов в строках UTF-32 простой, для этого используется целое число в качестве индекса, как обычно делается для строк ASCII.
Главный недостаток UTF-32 – это неэффективное использование пространства, так как для хранения символа используется четыре байта. Символы, лежащие за пределами нулевой (базовой) плоскости кодового пространства, редко используются в большинстве текстов.
В Unix-системах и веб-пространстве широкое применение находит формат представления Unicode, обозначаемый UTF-8. В этом формате для хранения одного символа используется от 1 до 4 байт (теоретически – до 6 байт). Одним из преимуществ UTF-8 является совместимость с ASCII – любые 7-битные символы ASCII отображаются как есть, а остальные выдают пользователю мусор (шум). Поэтому в случае, если латинские буквы и простейшие знаки препинания (включая пробел) занимают существенный объём текста, UTF-8 даёт выигрыш по объёму по сравнению с прочими представлениями Unicode.
В Windows распространена форма представления Unicode, называемая UTF-16 или UCS-2. В этой форме символы U+0000...U+FFFF (кроме U+D800..U+DFFF) записываются одним 16-битным словом, а остальные символы Unicode (U+10000...U+10FFFF) кодируются парой слов.
Коды UTF-32 и UTF-16 могут размещаться с разным порядком байт: little-endian – "младший первым" либо big-endian – "старший первым". Для UTF-8 проблема порядка байт отсутствует.
В новых реализациях библиотек языка Си при работе со строками национальных алфавитов используются следующие термины:
- Многобайтный символ (multibyte character), строка многобайтных символов
(multibyte string – MBS) – символ или текст в какой-то кодировке (обычно
специфичной для языка). Частный случай – 8-битные кодировки, в которых один символ
занимает один байт. Другой пример – кодировка UTF-8.
Строки MBS в Си представляются типом char*. В среде окружения программы есть такой параметр – "локаль", – по которому определяется системная кодировка. При вводе с клавиатуры (scanf) программа будет получать пользовательский текст в виде строки многобайтных символов именно в этой кодировке. При выводе на экран текста (printf) программа должна передавать сообщения в виде MBS-строк именно в этой кодировке. - "Широкий" символ (wide character), строка "широких" символов
(wide character string – WCS) – текст, в котором символы представлены
числом фиксированной разрядности (в Unix – 32 бита, в Windows – 16 бит).
Фактически это текст в Unicode UTF-32 (в Windows – UTF-16).
В для таких символов используется тип wchar_t, а для строк WCS – указатель (wchar_t*). Для ввода и вывода предлагается специальный набор функций, описанный в заголовочном файле wchar.h (wprintf, wscanf, ...). Для задания строковых констант в виде WCS используется специальный синтаксис (обратите внимание на букву L перед кавычкой):wchar_t *str = L"Строка широких символов";
Библиотека языка Си в значительной степени подразумевает использование MBS-строк, поскольку именно в таком виде обычно поступают в программу пользовательские данные. За исключением функций компьютерной лексикографии, такие строки прозрачно обрабатываются библиотечными функциями.
При старте программы в POSIX-совместимой среде в качестве локали выбирается "C" (соответствует 7-битной кодировке ASCII). Для корректной обработки MBS-строк рекомендуется установить локаль по значениям из среды окружения таким образом:
#include <locale.h> int main() { char *locale; locale = setlocale(LC_ALL, ""); ... }
Если лексикографическая обработка строк программе не требуется, то можно обойтись MBS-строками. В противном случае, одним из вариантов решения проблемы лексикографии будет использование WCS-строк.
Для конвертирования MBS-строки в WCS-строку используется функция mbstowcs(). Для обратного преобразования используется функция wcstombs(). Эти функции описаны в заголовочном файле stdlib.h. Для их правильного функционирования локаль программы должна быть корректно установлена.
size_t mbstowcs(wchar_t *dest, const char *src, size_t n)
size_t wcstombs(char *dest, const wchar_t *src, size_t n);
Для манипулирования WCS-строками используются функции-аналоги string.h:
string.h | wchar.h |
---|---|
size_t strlen(const char *s); Длина строки в байтах. |
size_t wcslen(const wchar_t *s); Длина WCS-строки в символах. |
char *strdup(const char *s); Резервирование памяти и копирование строки в новый буфер. |
wchar_t *wcsdup(const wchar_t *s); Резервирование памяти и копирование WCS-строки в новый буфер. |
char *strcpy(char *dest, const char *src); char *strncpy(char *dest, const char *src, size_t n); Копирование строки в предварительно зарезервированный буфер. |
wchar_t *wcscpy(wchar_t *dest, const wchar_t *src); wchar_t *wcsncpy(wchar_t *dest, const wchar_t *src, size_t n); Копирование WCS-строки в предварительно зарезервированный буфер. |
char *strcat(char *dest, const char *src); char *strncat(char *dest, const char *src, size_t n); Конкатенация строк. |
wchar_t *wcscat(wchar_t *dest, const wchar_t *src); wchar_t *wcsncat(wchar_t *dest, const wchar_t *src, size_t n); Конкатенация WCS-строк. |
int strcmp(const char *s1, const char *s2); int strncmp(const char *s1, const char *s2, size_t n); Сравнение строк (с учетом регистра). |
int wcscmp(const wchar_t *s1, const wchar_t *s2); int wcsncmp(const wchar_t *s1, const wchar_t *s2, size_t n); Сравнение WCS-строк (с учетом регистра). |
int strcasecmp(const char *s1, const char *s2); int strncasecmp(const char *s1, const char *s2, size_t n); Сравнение строк (без учета регистра). |
int wcscasecmp(const wchar_t *s1, const wchar_t *s2); int wcsncasecmp(const wchar_t *s1, const wchar_t *s2, size_t n); Сравнение WCS-строк (без учета регистра). |
char *strstr(const char *haystack, const char *needle); char *strchr(const char *s, int c); Поиск подстроки/символа в строке. |
wchar_t *wcsstr(const wchar_t *haystack, const wchar_t *needle); wchar_t *wcschr(const wchar_t *wcs, wchar_t wc); Поиск WCS-подстроки/символа в WCS-строке. |
Для ввода-вывода при подключении заголовочных файлов wchar.h и stdio.h доступны следующие функции:
stdio.h | wchar.h+stdio.h |
---|---|
int getc(FILE *stream); Прочитать 1 символ (байт) из файла. |
wchar_t getwc(FILE *stream); Прочитать 1 WCS-символ из файла. |
int getchar(void); Считать 1 символ (байт) из stdin. |
wchar_t getwchar(void); Считать 1 WCS-символ из stdin. |
int putc(int c, FILE *stream); Записать 1 символ (байт) в файл. |
wchar_t putwc(wchar_t wc, FILE *stream); Записать 1 WCS-символ в файл. |
int putchar(int c); Вывести 1 символ (байт) в stdout. |
wchar_t putwchar(wchar_t wc); Вывести 1 WCS-символ в stdout. |
char *fgets(char *s, int size, FILE *stream); Прочитать строку размером до n-1 байт из файла. |
wchar_t *fgetws(wchar_t *ws, int n, FILE *stream); Прочитать WCS-строку размером до n-1 WCS-символа из файла. |
int fputs(const char *s, FILE *stream); Записать строку в файл. |
int fputws(const wchar_t *ws, FILE *stream); Записать WCS-строку в файл. |
int printf(const char *format, ...); Форматированный вывод в stdout. |
int wprintf(const wchar_t *format, ...); Форматированный вывод в stdout. |
int fprintf(FILE *stream, const char *format, ...); Форматированный вывод в файл. |
int fwprintf(FILE *stream, const wchar_t *format, ...); Форматированный вывод в файл. |
int snprintf(char *str, size_t size, const char *format, ...); Форматированный вывод в строковой буфер. |
int swprintf(wchar_t *wcs, size_t maxlen, const wchar_t *format, ...); Форматированный вывод в WCS-буфер. |
int scanf(const char *format, ...); Форматированный ввод из stdin. |
int wscanf(const wchar_t *restrict format, ... ); Форматированный ввод из stdin. |
int fscanf(FILE *stream, const char *format, ...); Форматированный ввод из файла. |
int fwscanf(FILE *restrict stream, const wchar_t *restrict format, ... ); Форматированный ввод из файла. |
int sscanf(const char *str, const char *format, ...); Форматированный ввод из строкового буфера. |
int swscanf(const wchar_t *restrict ws, const wchar_t *restrict format, ... ); Форматированный ввод из WCS-буфера. |
Пример, иллюстрирующий разницу в представлении строк (в качестве MBS-кодировки использована UTF-8):
#include <locale.h> #include <stdio.h> #include <string.h> #include <ctype.h> #include <wchar.h> #include <wctype.h> int main() { int i; wchar_t wstr[] = L"У попа была собака..."; char str[] = "У попа была собака..."; setlocale(LC_ALL, ""); printf("%ls/%s\n", wstr, str); printf("size=%d/%d\n", sizeof(wstr), sizeof(str)); printf("len=%d/%d\n", wcslen(wstr), strlen(str)); /* toupper() / towupper() - перевод в верхний регистр: */ for (i=0; i<wcslen(wstr); i++) wstr[i] = towupper(wstr[i]); for (i=0; i<strlen(str); i++) str[i] = toupper(str[i]); printf("%ls/%s\n", wstr, str); return 0; }
На экран выведено:
$ ./a.out У попа была собака.../У попа была собака... size=88/37 len=21/36 У ПОПА БЫЛА СОБАКА.../У попа была собака...
Одна и та же последовательность из 21 символа кириллицы, знаков препинания плюс нулевой байт-терминатор в WCS-виде (UTF-32) занимает 88 байт, а в MBS-виде (UTF-8) занимает 37 байт. Однако количество символов при помощи strlen() из MBS-строки нельзя извлечь (мы получаем значение в байтах без символа-терминатора). Такое значение даёт функция wcslen() из WCS-строки. Также видно, что функция toupper() не работает на кириллице в MBS-строке.
В данном примере для вывода WCS-строки использована обычная функция printf(), однако указан специальный спецификатор вывода %ls. Эта возможность работает, только если правильно задана локаль.