Учебная деятельность    

Не-ASCII строки

Unicode in C and C++: What You Can Do About It Today

Учитывая всё разнообразие языков, существующих на Земле, текст – это не такой уж и тривиальный способ представления информации. Практически любому программисту приходится иметь дело с текстовыми сообщениями на каком-либо человеческом языке, поэтому следует принимать во внимание следующие проблемы:

Наиболее сложная часть этих проблем решена на уровне операционной системы, программных интерфейсов и библиотек. Если выбрана подходящая кодировка, то методы ввода и отображения символов, предалагаемые операционной системой, начинают корректно работать "сами по себе". Задача программиста в этом случае – правильно выбрать кодировку. По сравнению с прочими проблемами, проблема интернационализации не так критична, поскольку обычно не угрожает потерей или разрушением данных пользователя. А вот прозрачность имеющихся лексикографических методов сильно зависит от выбранной кодировки.

Долгое время в ходу были 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 проблема порядка байт отсутствует.

В новых реализациях библиотек языка Си при работе со строками национальных алфавитов используются следующие термины:

Библиотека языка Си в значительной степени подразумевает использование 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.hwchar.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.hwchar.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. Эта возможность работает, только если правильно задана локаль.