Первое, что нужно уметь делать с локализованными строками — это записывать символьные константы широких локализованных символов и отличать их от обычных строк char[]. Для этого строка записывается с предшествующим ей квалификатором L:
1 2 3 4 5 6 7 8 | #include <stdio.h> #include <wchar.h> #include <locale> int main( void ) { wchar_t w[] = L"русскоязычная строка"; setlocale( LC_ALL, "" ); printf( "%ls [%lu байт, %lu букв]\n", w, sizeof( w ), wcslen( w ) ); } |
Результатом будет:
1 2 | $ ./2 русскоязычная строка [84 байт, 20 букв] |
Обратим внимание, что длина строки (число символов) в этом случае отчётливо меньше, чем число байт выделенных под строку (в вашей операционной системе их отношение может отличаться от того, что я показываю в Linux, но это никак не влияет на технику программирования).
В такой строке рядом с равным успехом могут стоять символы самой разнообразной природы: различных языков, специальные математические символы, общепринятые в технике обозначения из греческого алфавита (α, ε, θ, π, σ, λ, φ, Ω…), музыкальные ноты и др. Как вы, очевидно, понимаете, точно также в составе строк широких символов, с равным успехом, могут встречаться и символы латинского алфавита (основная таблица ASCII), при этом каждый такой символ тоже будет занимать 2 или 4 байт (в зависимости от соглашений принятых в операционной системе), в отличие от привычного 1 байта.
Проделаем ряд операций с русскоязычными строками, но записывая их (пока) в традиционной форме массивов char:
1 2 3 4 5 6 7 8 9 10 | #include <string.h> #include <iostream> using namespace std; int main( void ) { char s1[] = "это ", s2[] = "фрагменты ", s3[] = "русскоязычной ", s4[] = "строки ", s5[ 120 ]; strcpy( s5, strcat( s1, strcat( s2, strcat( s3, s4 ) ) ) ); cout << s5 << endl; } |
Выполняем:
1 2 | $ ./3a это фрагменты русскоязычной строки [66] |
Казалось бы, что (почти) всё работает в точности как по учебнику и зачем нам какие-то широкие локализованные строки? Но это обманчивая иллюзия! Дело здесь в том, что некоторые традиционные строчные функции (strcat(), strcpy(), strdup(), strstr() и др.) будут возвращать корректный результат. Это потому что они выполняют операции над байтами, байт за байтом, не вникая во внутреннюю структуру копируемых символов.
Но другие операции (и ошибочный результат strlen() на это уже наглядно указывает) будут работать некорректно: strncpy(), strchr(), strsep(), strtok() и др. И они будут создавать вам очень неожиданные результаты, весьма сложные для толкования. Посмотрите как сработает побайтовый реверс строки, и как различается его работа на англоязычной и русскоязычной строке:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <string.h> #include <iostream> using namespace std; char* revers( char* s ) { for( int i = 0, j = strlen( s ) - 1; i < j; i++, j-- ) { char c = s[ i ]; s[ i ] = s[ j ]; s[ j ] = c; } return s; } int main( void ) { char se[] = "this is english string", sr[] = "это русскоязычная строка"; cout << revers( se ) << endl << revers( sr ) << endl; } |
Сработает это так, и это определённо не то, что вы рассчитывали получить:
1 2 3 | $ ./3b gnirts hsilgne si siht �коЀтс� �ѰнЇыѷЏѾкЁсур� �Ђэ� |
На этом мы закончим рассмотрение возможности представления русскоязычных строк традиционными массивами char[] и обработка их традиционными строчными функциями, и завершим это рассмотрение выводом: работать с русскоязычными строками как массивом char можно только:
а). либо когда мы используем строчные константы в неизменном виде, только как строки для их ввода-вывода в неизменном виде;
б). либо для обработки их функциями (библиотечными или собственными), которые не учитывают внутреннюю структуру символов, не вникая в содержимое строк, а оперируют с ними просто как с бессмысленной последовательностью байт.
Во всех остальных случаях корректная работа с кириллицей возможна только как с массивами широких локализованных символов wchar_t (с завершающим строку широким нулевым символом L’\0′). Для работы с таким представлением локализованных строк библиотека C предоставляет широкий набор строчных функций, полностью аналогичный традиционным строчным функциям, но вместо префикса str в их именах используется префикс wcs: wcslen() вместо strlen(), wcsncpy() вместо strncpy() и т.д.
Посмотрим как это работает на примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <stdio.h> #include <wchar.h> #include <locale> wchar_t* revers( wchar_t *w ) { wchar_t *sec, wb[ 80 ]; if( ( sec = wcschr( w, L' ' ) ) != NULL ) { 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 ); } return w; } int main( void ) { setlocale( LC_ALL, "" ); // только после этого работают преобразования! wchar_t ws[] = L"тестовая русскоязычная строка из нескольких слов "; while( L' ' == ws[ wcslen( ws ) - 1 ] ) ws[ wcslen( ws ) - 1 ] = L'\0'; printf( "устранение завершающих пробелов: '%ls'\n", ws ); printf( "1-е реверсирование слов: '%ls'\n", revers( ws ) ); printf( "2-е реверсирование слов: '%ls'\n", revers( ws ) ); } |
1 2 3 4 | $ ./4 устранение завершающих пробелов: 'тестовая русскоязычная строка из нескольких слов' 1-е реверсирование слов: 'слов нескольких из строка русскоязычная тестовая' 2-е реверсирование слов: 'тестовая русскоязычная строка из нескольких слов' |
Такой иллюстрации вполне достаточно для того, чтобы увидеть прямые аналогии использования функций работы с символами wchar_t. Тот, кто имеет некоторый опыт работы со строками char без усилий перенесёт его широкие строки. Установка языковой локали (вызов setlocale()) устройства вывода (терминала) — обязательна, потому что по умолчанию программа C/C++ устанавливает локаль “C” (так сложилось исторически), которая допускает вывод только 128 символов младшей половины 8-битной таблицы ASCII.
В показанном написании функция устанавливает локаль, используемую в операционной системе по умолчанию — я предполагаю, что мы экспериментируем в русскоязычной установленной системе. Новый стандарт языка (C99) вводит и новый формат для функций форматирования строк (printf(), sprintf()) — %ls, это форматирование строк wchar_t[].
Точно так же, как и с массивами char, перешедшими в C++ из C, библиотека C++ вводит полный аналог контейнерного класса string, но содержащий в своём составе широкие локализованные символы, и носит название этот класс wstring:
1 2 3 4 5 6 7 8 | #include <locale> #include <iostream> using namespace std; int main( void ) { locale::global( locale( "" ) ); wstring ws = L"строка"; wcout << ws << endl; } |
Здесь вывод строки локализованных символов (ws) должен выводится в поток вывода wcout (аналогичный по смыслу cout, но отличный от cout).
В показанном написании: locale::global( locale( “” ) ) — это установка локали по умолчанию в ООП манере C++, аналогично тому, как это было показано раньше в манере C.
Вопросы ввода-вывода строк широких символов (на терминал или в файл) отдельный непростой предмет, поэтому его рассмотрение мы отложим до отдельной заметки на этот счёт.