Кодировка символов — способ записи последовательности символов (строки) с помощью последовательности натуральных чисел (“кодов”). В простейшем случае кодировка задаётся таблицей символов, в которой перечислены все доступные символы, и каждому из них присвоен код (номер). Как правило, в более сложных случаях такая таблица всё равно есть, но кодировка включает в себя особенности оформления кодов с целью уменьшения размера сообщения в байтах.
ASCII (от American Standard Code for Information Interchange — “американский стандартный код для обмена информацией”) — кодировка символов, получившая широкое распространение.
Кодировка ASCII содержит 128 символов, каждому из которых соответствует своя последовательность семи бит (двоичная запись номера символа).
Первые 32 кода и последний (символ 127) выделены для специальных управляющих символов. Большинство этих кодов были важны во времена использования телетайпов и других подобных устройств, напрямую управлявшихся последовательностью переданных на них символов, и в настоящее время практически не используются. Символ 127 (DEL
) использовался для уничтожения содержимого перфоленты при записи поверх старого содержимого (код 127 соответствует семи пробитым отверстиям).
Номер | Номер16 | Символ | Запись в C | Смысл |
---|---|---|---|---|
0 | 00 | null character NUL |
\0 |
нулевой символ — конец си-строки |
7 | 07 | bell BEL |
\a |
звуковой сигнал |
8 | 08 | backspace BS |
\b |
сдвинуть каретку влево на одну позицию |
9 | 09 | horizontal tabulation TAB |
\t |
сдвинуть каретку вправо до следующей колонки |
10 | 0A | line feed LF |
\n |
сдвинуть каретку вниз на одну строку (новая строка) |
11 | 0B | vertical tabulation VT |
\v |
сдвинуть каретку вниз до следующей группы строк |
12 | 0C | form feed FF |
\f |
подать новый лист |
13 | 0D | carriage return CR |
\r |
вернуть каретку на начало строки |
27 | 1B | escape ESC |
\x1b |
переключиться в режим приёма управляющей последовательности |
127 | 7F | delete DEL |
\x7f |
удалить следующий символ |
Замечания
LF
в текстовый поток вывода по стандартам C и C++ выводится последовательность, соответствующая новой строке (один символ LF
в современных Unix-подобных системах или пара символов CR LF
в Windows).VT
, FF
, ESC
и DEL
при выводе в консоль обычно или игнорируются или выводятся некоторым графическим символом.BS
, TAB
, ESC
и DEL
соответствуют одноимённым клавишам на стандартной “IBM PC”-клавиатуре и могут использоваться как коды, передаваемые при нажатиях этих клавиш.BS
в консоль обычно происходит перемещение курсора назад, что позволяет следующим выведенным символом затереть предыдущий. При реальной печати на бумаге смещение каретки назад могло использоваться для имитации полужирного шрифта (пропечатать один текст дважды), совмещения символов (можно получить символы вроде Ø, â, Ƚ, ǹ и т.п. — для этого такие символы как ^
, ~
располагались в шрифтах выше букв), подчёркивания (пропечатать символ _
поверх подчёркиваемого текста).Полную таблицу см. здесь.
Номера | Номера16 | Символы | Запись в C |
---|---|---|---|
32 | 20 | пробел | пробел |
34 | 22 | двойные кавычки " |
\" |
39 | 27 | кавычка ' |
\' |
48–57 | 30–39 | цифры 0–9 | 0 –9 |
65–90 | 41–5A | заглавные латинские буквы A–Z | A –Z |
92 | 5C | обратная косая черта \ |
\\ |
95 | 5F | знак подчёркивания | _ |
97–122 | 61–7A | строчные латинские буквы a–z | a –z |
Замечания
Один восьмибитный байт может принимать 256 различных значений. При передаче символов ASCII один символ кодируется одним байтом, при этом старший бит устанавливается в ноль, а семь младших бит содержат код символа. Таким образом, задействуется лишь половина доступного диапазона. Это открывает возможность расположить в следующих 128 кодах какие-то другие символы. В итоге появилось множество различных ASCII-совместимых кодировок, различающихся “верхними” (с установленным старшим битом) 128-ю символами, например, “европейская” Latin-1 и кириллические КОИ-8, IBM CP866, Windows-1251. Выбор кодировки заключается в смене различающихся 128 символов, которые называют “кодовой страницей” codepage. Интерпретатор командной строки cmd.exe в Windows позволяет переключать текущую страницу командой chcp
, по умолчанию для кириллицы в консоли используется страница 866 (совместимость с MS-DOS).
К сожалению, такие восьмибитные кодировки не могли решить проблему как кодирования письменностей, в которых большое количество знаков (например, китайская) или записи текстов, включающих символы из разных письменностей.
Юникод Unicode — группа стандартов, посвящённых кодированию и передаче текстов на электронных носителях в символьной форме. Фактически состоит из двух частей: стандартизованного набора знаков (Universal Character Set, UCS) и кодировок (Unicode Transformation Format, UTF). UCS основан на символьном подходе в противовес графическому подходу, т.е. даже если символы из разных письменностей выглядят одинаково, они считаются разными (например, латинское C и кириллическое С). UCS разбит на пронумерованные “плоскости” planes, собирающие символы в группы, но нумерация самих символов сплошная. Первые 128 символов совпадают с кодировкой ASCII. Ряд “символов” UCS представляет собой не отдельные символы, а части символов, графически комбинируемые с соседними символами. Кроме того, есть и управляющие символы, в том числе, доставшиеся в наследство от ASCII. Поэтому элементы UCS принято называть кодовыми позициями codepoints. Зарезервированный объём UCS составляет немногим больше одного миллиона символов, используемый — всего порядка 110 тысяч. Недостатком Юникода является то, что одно и то же изображение часто может быть получено различными способами (помимо визуально совпадающих знаков различных письменностей, это могут быть символы-комбинации знаков, например, буквы й и ё).
Кодировка UTF-32 (UTF-32BE для BE-порядка байт, UTF-32LE для LE-порядка байт) передаёт каждую кодовую позицию 4-байтным целым (номером в UCS, поэтому UTF-32 также обозначают как UCS-4). Эта кодировка сравнительно удобна для работы с Юникод-текстами в памяти, но считается недостаточно компактной для задач хранения и передачи текстов. К сожалению, даже в случае UTF-32 одному знакоместу (графическому изображению символа) на экране может соответствовать больше одного 4-байтного кода, его формирующих.
Кодировка UTF-8 передаёт каждую кодовую позицию последовательностью байт (от 1 до 4). При этом, текст в кодировке ASCII является текстом в кодировке UTF-8 (обратная совместимость), а для представления дополнительных символов используются байты с установленным старшим битом. Данная кодировка на данный момент является самым распространённым способом передачи текстов по сети Интернет. Её преимуществами, в частности, являются компактность представления символов ASCII и независимость от порядка байт (нет проблемы big endian vs little endian).
Тема работы с Юникодом (и строковыми данными вообще) очень обширна и содержит множество тонкостей, здесь рассматриваются лишь некоторые простые элементы в аспекте программирования на C и C++. Для серьёзного изучения как Юникода, так и затронутых здесь разделов стандартной библиотеки C++ следует обратиться к специализированным источникам.
В языке C первоначально был только один встроенный тип, предназначенный для представления символов — тип char, “по совместительству” являющийся представлением байта, как минимальной отдельно адресуемой единицей памяти. Символьные кодировки не входят в стандарты C и C++ и являются прерогативой системы.
В начале развития стандарта Юникод в C++ был введён новый тип символа — wchar_t (т.е. “широкий символ” wide character), по определению являющийся целочисленным типом с диапазоном, достаточным для представления всех символов, доступных на данной системе. Так как поначалу предполагалось, что Юникоду “хватит” 16 бит для представления всех номеров UCS, в некоторых системах (например, Windows) был закреплён 16-битный wchar_t. В современных GNU/Linux-системах используется 32-битный wchar_t, подходящий для кодировки UTF-32.
В связи с введением в обиход различных кодировок UTF и отсутствием совместимости между различными системами при использовании wchar_t в C++11 были добавлены два новых символьных (целочисленных) типа: char16_t для 16-битных кодировок (UCS-2, UTF-16) и char32_t для 32-битных кодировок (UTF-32). Возможно введение в будущем стандарте типа char8_t, пока же для представления единиц кодирования строк в кодировке UTF-8 используется тип char.
Символьный литерал — представление константы символьного типа непосредственно в тексте программы. Символьные литералы записываются в одинарных кавычках и должны содержать один символ, который задаётся либо непосредственно, либо с помощью специальной экранированной последовательности escape sequence, открываемой символом \.
Стандартные экранированные последовательности для некоторых управляющих символов, а также специальных символов (обратная косая черта, кавычки) приведены в таблицах выше в колонках под заголовком “Запись в C”.
Экранированные последовательности позволяют указать код символа числом: в шестнадцатеричной системе после префикса x
или в восьмиричной системе (без префикса), например, ‘\x5c’ — то же, что ‘\134’ — то же, что ‘\\’, а именно сам символ “обратная косая черта”.
При записи непосредственно символа в литерал, компилятор может оставить код в той кодировке, в которой символ был введён, либо преобразовать в некоторую кодировку “по умолчанию”. Например, исходный код может быть набран в кодировке UTF-8, в которой кириллические знаки записываются двухбайтными кодами, но компилятор Visual C++ при оформлении символьного литерала типа char (один байт) преобразует их к кодировке Windows-1251.
Выбор конкретного типа символа осуществляется с помощью префикса.
Тип | Префикс | Пример | Стандарт | Диапазон |
---|---|---|---|---|
char | нет | 'a' |
C90 | системная кодировка |
wchar_t | L |
L'ф' |
C++98 | системная кодировка |
char16_t | u |
u'ф' |
С++11 | UCS-2 |
char32_t | U |
U'∀' |
C++11 | UCS-4 |
char8_t | u8 |
u8'z' |
C++17 (план) | ASCII |
Строковый литерал — представление константной последовательности символов непосредственно в тексте программы. Строковые литералы записываются в двойных кавычках и могут не содержать ни одного символа. Максимальное допустимое количество символов с строковом литерале определяется компилятором.
Каждому строковому литералу соответствует константный массив соответствующего символьного типа, содержащий символы литерала в соответствующей символьному литералу кодировке плюс завершающий нулевой символ. Поэтому реальный размер такого массива, по крайней мере, на единицу больше числа символов в нём.
Строковый литерал может содержать такие же экранированные последовательности, как символьный литерал. Каждой экранированной последовательности соответствует один символ.
Расположенные подряд строковые литералы, разделённые лишь пробельными символами и комментариями, автоматически конкатенируются в один строковый литерал. Например:
// Выводится одна строка, а не три.
cout << "First line\n" // \n должны быть выставлены явно
"Second line\n"
"Ok, finally the third line.";
Префикс | Пример | Кодировка | Тип символа | Стандарт |
---|---|---|---|---|
нет | "two words" |
системная | char | C90 |
L |
L"два слова" |
системная | wchar_t | C++98 |
R |
R"!(C:\User\)!" |
см. ниже | см. ниже | C++11 |
u |
u"∀ε>0 ∃δ " |
UTF-16 | char16_t | C++11 |
U |
U"A∨¬A" |
UTF-32 | char32_t | C++11 |
u8 |
u8"escape-знак" |
UTF-8 | char8_t | C++11 |
Префикс R
(от англ. raw — “сырой”) позволяет ввести строку без экранирования символов: все символы между R“ разделитель ( и ) разделитель ” включаются в литерал. Здесь разделитель — произвольная последовательность символов, которая выбирается так, чтобы она (с закрывающей скобкой) не встречалась в самой строке. Перед префиксом R
можно поставить префикс, определяющий тип символа и кодировку.
Си-строка или нуль-терминированная строка — структура данных, представляющая собой массив символов, последний из которых (и только он) является нулевым. Так как нулевой символ маркирует конец си-строки, для передачи её в функцию достаточно передать указатель на её начало. Любой указатель на символ некоторой си-строки является указателем на её подстроку — тоже си-строку.
Заголовочный файл cstring содержит базовый функционал для работы с си-строками и массивами символов (байт). Массивы байт передаются парой (указатель (типа void*
), размер (в байтах)). Далее приведены краткие описания некоторых стандартных функций, объявленных в этом заголовочном файле.
Для краткости даны объявления функций только для языка C. В случае C++ нередко существуют аналогичные пары функций с различной константностью (C “теряет” константность), например, в C:
void*
memchr (const void* mem, int byte, size_t count)
— теряет свойство “только для чтения” блока mem
.В то же время в C++ (в пространстве имён std) имеется сразу два варианта:
void*
memchr (void* mem, int byte, size_t count)
— блок, допускающий запись;const void*
memchr (const void* mem, int byte, size_t count)
— блок только для чтения.void*
memset (void* dest, int byte, size_t count)
заполняет блок памяти с адресом dest
длиной count
байт значением byte
(приведённым к unsigned char
). Возвращает dest
.void*
memcpy (void* dest, const void* src, size_t count)
копирует содержимое блока памяти с адресом src
длиной count
байт по адресу dest
. Возвращает dest
. В случае, если диапазоны [src
, src
+count
) и [dest
, dest
+count
) имеют непустое пересечение, поведение не определено.void*
memmove (void* dest, const void* src, size_t count)
действует аналогично memcpy
, но также обрабатывает случай пересекающихся диапазонов. Семантика в этом случае эквивалентна копированию через временный промежуточный буфер. Наличие двух разных функций memcpy
и memmove
связано с тем фактом, что memcpy
может быть быстрее благодаря предположению, что диапазоны не пересекаются.void*
memchr (const void* mem, int byte, size_t count)
ищет в блоке памяти с адресом mem
длиной count
байт первое с начала блока вхождение байта byte
(приводится к unsigned char
). Если такой байт найден, то функция возвращает его адрес. В противном случае возвращает нулевой указатель.int
memcmp (const void* lhs, const void* rhs, size_t count)
сравнивает побайтно лексикографически (“словарно”) два блока памяти (с адресами lhs
и rhs
) одинаковой длины count
байт. Возвращает –1, если блок lhs
стоял бы в словаре до rhs
; 0, если блоки равны; и +1, если бы блок lhs
стоял бы в словаре после rhs
. При сравнении порядок байт относительно друг друга определяется их числовыми значениями, а не смыслом соответствующих им символов.Все эти функции порождают неопределённое поведение, если в качестве параметра, подразумевающего передачу адреса си-строки с завершающим нулём, передаётся указатель, не указывающий на си-строку.
size_t
strlen (const char* str)
возвращает длину си-строки с адресом str
, т.е. по сути расстояние от str
до первого вхождения нулевого символа, измеренное в байтах.char*
strcpy (char* dest, const char* src)
копирует строку с адресом src
(вместе с завершающим нулём) в буфер по адресу dest
, возвращает dest
. Поведение не определено, если выделенный буфер недостаточно большой (выделен слишком маленький блок памяти, либо диапазон области копирования пересекается с исходной строкой).char*
strcat (char* dest, const char* src)
дописывает копию си-строки по адресу src
к си-строке по адресу dest
. Т.е. выполняет конкатенацию строк. Блок памяти, в котором хранится строка по адресу dest
должен иметь достаточный размер, чтобы вместить конкатенацию. Возвращает dest
.char*
strchr (const char* str, int byte)
ищет в си-строке по адресу str
первое вхождение байта со значением byte
. Возвращает указатель на найденный байт, либо нулевой указатель, если найти заданный байт не удалось.char*
strrchr (const char* str, int byte)
ищет в си-строке по адресу str
последнее вхождение байта со значением byte
. Возвращает указатель на найденный байт, либо нулевой указатель, если найти заданный байт не удалось.int
strcmp (const char* lhs, const char* rhs)
сравнивает две си-строки лексикографически, возвращает –1, если строка lhs
стояла бы в словаре до rhs
; 0, если строки равны; +1, если бы lhs
стояла бы в словаре после rhs
. При сравнении порядок байт относительно друг друга определяется их числовыми значениями, а не смыслом соответствующих им символов.char*
strstr (const char* str, const char* substr)
ищет в си-строке по адресу str
первое вхождение подстроки, заданной си-строкой с адресом substr
. Возвращает указатель на первый символ найденной подстроки, либо нулевой указатель, если такой подстроки нет.Также существуют аналоги данных функций для работы с wchar_t и многобайтными кодировками.
Возможности C++ позволяют определить тип данных, объекты которого самостоятельно управляют массивом символов (строкой) и предоставляют набор удобных в использовании базовых операций.
Тип строки | Тип элемента | Стандарт |
---|---|---|
string | char | C++98 |
wstring | wchar_t | C++98 |
u16string | char16_t | C++11 |
u32string | char32_t | C++11 |
Все перечисленные строковые типы по существу устроены одинаково и предоставляют одинаковые с точностью до типа данных наборы операций. Фактически они являются объектами, управляющими динамическими массивами символов, в которых и хранятся строки. В отличие от си-строк, данные объекты позволяют хранить произвольные символы, включая нулевые. Строки можно сравнивать с помощью обычных операций сравнения вроде ==
и <
(используется лексикографический порядок).
Далее приведены примеры использования некоторых возможностей стандартных строк.
// Переменную строкового типа можно инициализировать литералом соответствующего типа
// или просто нуль-терминированной си-строкой (указателем на char).
string s = "text";
// В C++14 с помощью суффикса s можно явно указать,
// что строковый литерал задаёт объект string
// (или аналогов в случае наличия префиксов).
string with_null_chars = "\0here is some text\0"s;
cout << with_null_chars; // попробуйте сравнить с выводом в случае отсутствия суффикса s
// При присваивании переменных строки копируются.
s = with_null_chars;
assert(s == with_null_chars);
// К символам (кодовым позициям) в строке можно обращаться по индексу как к массиву.
s[0] = '!'; // заменили символ в позиции 0 в копии
assert(s != with_null_chars);
// Строки "знают" свой размер:
assert(s.size() == 19);
// Прямая проверка на пустоту (т.е. на равенство размера нулю):
assert(!s.empty());
// Очистить строку можно 1) присвоив "", 2) вызвав clear:
with_null_chars.clear();
assert(with_null_chars.empty());
// Указатель на массив символов, принадлежащий объекту строки, в формате си-строки
// (например, чтобы задействовать функции из стандартной библиотеки C) можно получить
// с помощью функции c_str:
assert(strchr(s.c_str(), 'h') != nullptr);
// Размер строки можно изменять: при уменьшении лишние с конца символы удаляются,
// при увеличении в конец строки нужное количество раз добавляется заданный символ.
s.resize(3);
assert(s == "!he");
s.resize(5, 'e'); // если не указать символ, то будет добавлен нулевой символ
assert(s == "!heee");
// Конкатенация через + и +=, может добавлять символы и литералы.
s += 'w';
assert(s == "!heeew");
// Извлечь копию подстроки с заданной позиции и с заданной длиной можно функцией substr:
assert( (s.substr(1, 2) + "at") == "heat" );
В заголовочном файле string также определены удобные функции преобразования между числами и их строковыми представлениями:
(const
(w
)string &s, size_t *pos = nullptr, int radix = 10)
распознают содержимое строки s
как целое число (типов int, long, long long, unsigned long и unsigned long long соответственно — два последних варианта не принимают знак числа), читая её с начала до момента первой ошибки (или конца числа). По адресу pos
(если это не нулевой указатель) сохраняется индекс первого символа за считанным числом. Значение radix
задаёт основание системы счисления (принимаются основания от 2 до 36). Если в качестве значения radix
передать 0, то основание будет распознаваться по префиксу аналогично целочисленным литералам (0 — основание 8, 0x/0X — 16, другое — 10; двоичные 0b/0B пока не вошли в стандарт по этим функциям, так как они определены через стандартные функции C strtol, strtoll, strtoul и strtoull, а в стандарт C двоичные литералы пока не входят).(const
(w
)string &s, size_t *pos = nullptr)
распознают содержимое строки s
как число с плавающей точкой (типов float, double и long double соответственно), читая её с начала до момента первой ошибки (или конца числа). По адресу pos
(если это не нулевой указатель) сохраняется индекс первого символа за считанным числом. Числа распознаются в соответствии с правилами записи литералов, кроме того, принимаются строки INF
или INFINITY
(без учёта регистра) как значение “бесконечность” и NAN
как значение “нечисло”.(
что-то)
возвращает строковое представление (в виде объекта string и wstring соответственно) значений встроенных типов.assert(stoi(to_string(10)) == 10);
assert(stod(to_string(3.14)) == 3.14);
Класс string предоставляет набор конструкторов, некоторые из которых могут пригодиться, например, при выполнении заданий из самостоятельной 11.
Примеры даны в двух вариантах: определение переменной типа string и создание временного объекта типа string.
// Конструктор по умолчанию создаёт пустую строку.
string s1;
cout << string(); // создать пустую строку в выражении.
// Конструктор, создающий строку повторением символа заданное число раз.
string s2(5, '*');
cout << string(5, '*'); // выведет *****
// Конструктор из си-строки.
string s3("nevermore!"); // или s3 = "nevermore!"
// Конструктор из суффикса другой строки (только для string, не си-строки!).
string s4(s3, 5); // 5 -- позиция первого символа префикса.
cout << string(s3, 5); // выведет more!
// Конструктор из подстроки другой строки.
string s5(s3, 5, 4); // 5 -- позиция начала, 4 -- длина подстроки.
cout << string(s3, 5, 4); // выведет more
// Конструктор из массива символов: надо передать указатель и размер.
char block[] { 1, 2, 3, 4 };
string s6(block, sizeof(block));
cout << string(block, sizeof(block)); // что выведет?
// Конструктор из диапазона символов, заданного парой итераторов.
string s7(istream_iterator<char>{cin}, istream_iterator<char>{});
cout << string(istream_iterator<char>{cin}, istream_iterator<char>{});
// -- прочитать весь ввод с cin, пропуская пробелы, и собрать в строку.
Вставка подстрок может осуществляться с помощью функций append
(в конец) и insert
(на произвольную позицию).
Удаление подстроки может быть выполнено функцией erase
.
Замена подстрок (с заданным положением, а не содержанием) выполняется с помощью функции replace
.
// Вставка и удаление
string alpha = "al";
alpha.append("pha"); // то же, что alpha += "pha".
assert(alpha == "alpha");
// Добавить префикс заданной длины из си-строки.
alpha.append("same", 1);
assert(alpha == "alphas");
// Добавить подстроку объекта string с заданной позиции заданной длины.
string sigma = "sigma";
alpha.append(sigma, 1, 3);
assert(alpha == "alphasigm");
// Добавить заданное количество символов.
alpha.append(2, '+'); // сначала количество, потом символ.
assert(alpha == "alphasigm++");
// Удалить с заданной позиции заданное число символов.
alpha.erase(0, 4); // с нуля == удалить префикс.
assert(alpha == "asigm++");
// Удалить суффикс с заданной позиции.
alpha.erase(2);
assert(alpha == "as");
// Вставить заданное число символов в заданную позицию.
alpha.insert(1, 4, '*');
assert(alpha == "a****s");
// Вставить заданную строку в заданную позицию.
alpha.insert(3, sigma);
assert(alpha == "a**sigma**s");
// Или подстроку другой строки.
sigma.insert(2, alpha, 0, 3);
assert(sigma == "sia**gma");
// Удалить всё содержимое строки.
alpha.clear(); // то же, что alpha.erase() или alpha = "".
assert(alpha == "");
// Замена подстрок
string word = "orthographia";
// Заменить подстроку с заданной позицией и длиной на другую строку.
word.replace(5, 5, "dox");
assert(word == "orthodoxia");
string other = "Sparax";
// Заменить подстроку на подстроку другой строки.
// (Эффективность выше, чем при использовании substr).
word.replace(0, 5, other, 1, 4);
assert(word == "paradoxia");
// В качестве длины второй подстроки можно указать специальное значение string::npos,
// в этом случае в качестве подстроки будет взят весь остаток строки.
word.replace(0, 4, other, 1, string::npos);
assert(word == "paraxdoxia");
// string::npos можно указать и в качестве длины заменяемой подстроки,
// тогда будет осуществлена замена всего остатка (суффикса, "хвоста") строки.
word.replace(5, string::npos, other, 1, string::npos);
assert(word == "paraxparax");
// Вместо строки можно указывать символ, которым будет затёрта заданная подстрока.
// Третий параметр -- число символов (т.е. подстрока длины 4 заменяется на три звёздочки).
word.replace(3, 4, 3, '*');
assert(word == "par***rax");
Объекты string поддерживают несколько функций поиска (подстрок и символов). Все эти функции возвращают индекс первого символа, отвечающего найденному вхождению, либо специальное значение string::npos
, если ничего не было найдено.
string text = "alpha beta gamma delta epsilon alpha";
// Можно искать другую строку как подстроку.
assert(text.find("beta") == 6);
assert(text.find("zeta") == string::npos);
// Можно искать, начиная не с начала, а с заданной позиции,
// что удобно для нахождения всех вхождений подстроки.
assert(text.find("beta", 7) == string::npos);
assert(text.find("alpha") == 0);
assert(text.find("alpha", 10) == 31);
// Если дан массив символов, а не си-строка, то следует указать его длину
// дополнительным параметром.
char delta[] = { 'd', 'e', 'l', 't', 'a' };
assert(text.find(delta, 0, 5) == 17);
// Наконец, можно искать конкретный символ.
assert(text.find('a') == 0);
assert(text.find('z') == string::npos);
assert(text.find('a', 1) == 4);
// Аналогичный find набор возможностей предоставляет функция
// rfind,
// её отличие заключается в том, что она ищет
// последнее (самое правое) вхождение вместо первого (самого левого).
// При этом позиция начала поиска исключает из поиска правую часть
// строки (суффикс), а не левую (префикс) как в случае find.
assert(text.rfind("alpha") == 31);
assert(text.rfind("alpha", 10) == 0);
// text = "alpha beta gamma delta epsilon alpha"
// Если надо найти самое левое вхождение некоторого символа
// из заданного набора, то следует воспользоваться функцией
// find_first_of
assert(text.find_first_of("bcd") == 6);
assert(text.find_first_of("bcd", 7) == 17);
// Напротив, если требуется найти символ, НЕ принадлежащий
// заданному набору, то следует воспользоваться функцией
// find_first_not_of
assert(text.find_first_not_of("abcdef") == 1);
assert(text.find_first_not_of("abcdef", 10) == 10); // пробел
// Наконец, можно искать не слева направо, а справа налево.
// Соотвественно: find_last_of и find_last_not_of
assert(text.find_last_of("bcd") == 17);
assert(text.find_last_not_of("abcdef") == 34); // h
Вывод текстового представления значения value в stdout выполняется с помощью оператора <<
std::cout << value;
Записать в поток один символ можно или оператором << или функцией put. Считать один символ можно с помощью функции get.
// Вариант 1, получить символ как результат функции,
// если считать символ невозможно, возвращает специальное значение EOF.
// EOF является целым числом вне диапазона значений кодов символов,
// поэтому его присваивание переменной ch является, вообще говоря, ошибкой.
// Данный вариант используется только если достоверно известно, что поток не кончился.
ch = cin.get();
// Вариант 2, если возможно, считать и записать символ в переменную,
// переданную по ссылке. В противном случае не изменять эту переменную.
// Здесь функция get возвращает исходный объект потока (в данном случае сам cin),
// предварительно выставив флаги ошибок, если они произошли.
if (cin.get(ch)) // Если не было ошибок,
cout.put(ch); // вывести то, что удалось считать.
Если использовать для считывания символов вместо функции get оператор >>
, то поток будет пропускать все пробельные символы. Проверьте, например, как работает следующий код:
// Типичная идиома для считывания последовательности: цикл for.
// Результатом выражения (cin >> ch) является сам cin,
// поэтому данный цикл выполняется до первой ошибки ввода (или конца ввода).
for (char ch; cin >> ch;)
cout.put(ch);
Считать “слово”, т.е. пропустить все пробельные символы и считать последовательность непробельных символов до первого пробельного или конца файла (сочетание +-,.!
тоже будет считаться “словом”) можно с помощью std::cin >> word
, где word
имеет тип string
.
Считать всё до перевода строки можно с помощью std::getline(std::cin, line)
, где line
— тоже строковая переменная. Чтобы читать из потока до заданного символа ch
, можно указать третий параметр: std::getline(std::cin, line, ch)
. Функция getline
забирает символ-разделитель (перевод строки или значение ch
) из потока ввода, но не дописывает его к считанной строке.
Далее идёт листинг, демонстрирующий некоторые возможности средств текстового ввода-вывода стандартной библиотеки C++. Возможно, не всё из этого будет понятно на данном этапе, однако, оно может пригодиться в дальнейшем.
// Считать строчку до перевода строки:
string text;
getline(cin, text);
// Считать (один) следующий символ:
char ch = cin.get();
// Положить обратно символ вместо ранее считанного
// (вместо ch мог стоять любой символ):
cin.putback(ch);
// Положить назад именно тот символ, что был считан:
cin.unget();
// Получить следующий символ, не убирая его из потока:
ch = cin.peek();
// Считать следующий непробельный символ
// (пробельные символы пропускаются):
cin >> ch;
// Проверить был ли достигнут конец файла:
bool ended = cin.eof();
// Проверить были ли ошибки ввода (например, пытались считать число, а в потоке были буквы):
bool failed = cin.fail();
// Проверить внешние ошибки (например, привязанный к stdin файл был удалён системой во время работы
// или произошёл сбой оборудования, кроме того, данный бит ошибки устанавливается при некоторых недопустимых действиях программы):
bool external_error = cin.bad();
// Наоборот, проверить, что ошибок не было:
bool ok = cin.good();
// Сбросить флаги ошибок, в том числе признак конца файла:
cin.clear();
// Вывести все символы буфера потока на конечное устройство (очистить буфер):
cout.flush();
// Вывести перевод строки и вызвать flush():
cout << endl;
// Передать в поток один символ с заданным кодом:
char ch = 111;
cout.put(ch);
// -- или -- (отличия более тонкие, чем в случае с вводом):
cout << ch; /* Может выполнить "расширение" символа,
чтобы соответствовать особым требованием ОС,
например, в Windows перевод строки оформляется парой символов с кодами 13 и 10. */
// Выбрать количество знаков после точки при выводе
// чисел с плавающей точкой:
cout.precision(3); // точность -- три значащих цифры
cout << 1.12345; // > 1.12
“Задержка экрана” в простых случаях может быть выполнена с помощью чтения символа с cin (функции get
и ignore
). Если буфер ввода пуст, то исполнение программы остановится до тех пор, пока пользователь не введёт что-нибудь.
// Считать один символ и отбросить его:
cin.get();
// Название функции ignore подчёркивает, что символ не нужен.
cin.ignore();
// Если надо пропустить известное число символов, то можно передать его функции ignore:
cin.ignore(10); // отбросить 10 символов
К сожалению, такой нехитрый приём не работает в ряде случаев, когда поток ввода содержит неопределённое количество “мусора”. Более универсальный метод представлен в примере по ссылке.
// Подключить файловые потоки ввода-вывода:
#include <fstream>
// Создать объект потока-ввода:
ifstream f;
f.open("my.txt"); // открыть файл с именем "my.txt"
// Создать объект потока ввода, читающий файл filename (переменная типа string или char*):
ifstream f(filename);
f.close(); // закрыть файл
// Создать объект потока вывода, пишущий файл filename (заново):
ofstream f(filename);
// Либо открыть для дописывания (не уничтожая старое содержимое):
ofstream f(filename, ios::app); // app от append
// Проверить, открыт ли файл:
if (f.is_open())
cout << "f is opened";
// Проверить, что флаги ошибок сброшены:
if (f) // проверка также пройдёт успешно, если мы не пытались открыть файл
cout << "f is good";
В целом, можно считать, что объекты ifstream ведут себя как cin, а объекты ofstream ведут себя как cout.
Файл позволяет произвольно изменять позицию чтения (для ifstream) и позицию записи (для ofstream).
// Узнать текущую позицию записи в файле
// (== расстоянию в байтах от начала файла):
auto f_pos = f.tellp();
// Установить позицию записи после первых 10 байт:
f.seekp(10);
Для позиции чтения существуют аналогичные функции tellg
и seekg
. Функции seekg
и seekp
кроме варианта с абсолютным смещением имеют вариант со смещением относительно трёх возможных позиций: начала ios::beg
, текущей позиции ios::cur
и конца ios::end
. Это позволяет, например, узнать размер открытого файла:
// Тип streampos -- целочисленный тип, достаточный для того, чтобы представлять позицию или смещение в файле.
// Установим позицию в конец файла и узнаем, чему она равна -- это и будет размер файла.
// Данная функция работает не только для файлов.
streampos file_size(istream &s)
{
// Сохраним текущую позицию, чтобы восстановить её после измерения размера.
const auto old_pos = s.tellg();
s.seekg(0, ios::end); // Смещение 0 от конца.
const auto size = s.tellg();
s.seekg(old_pos); // Вернуться на исходную позицию.
return size;
}
Допускается открытие одного и того же файла сразу несколькими объектами ifstream. Каждый из них будет читать данные со своей позиции чтения.
// Подключить потоки вывода в строку.
#include <sstream>
// Допустим, в fn находится фрагмент имени некоторого файла:
string fn = "dataset.bin";
// Нужно открыть файл с именем fn, дополненным "." и номером n.
// Поток вывода в строку (хранится внутри) позволит "собрать" нужное имя.
ostringstream newfn;
// Сформировали нужную строку:
newfn << fn << '.' << n;
// str() возвращает строку из объекта потока, можно открыть файл:
ofstream outf(newfn.str());
// С другой стороны, конкретно этот пример можно записать короче
// с помощью функции to_string, появившейся в C++11:
ofstream outf(fn + "." + to_string(n));
Аналогично, можно считать, что объекты istringstream ведут себя как cin, а объекты ostringstream ведут себя как cout. Функция-член str()
позволяет обращаться к содержимому потока как к строке типа string: str() возвращает копию содержимого потока в виде строки, а str(строка) устанавливает новое содержимое потока.
// Чтение последовательности векторов через istringstream.
// Каждый вектор занимает свою строку, поэтому читаем строку в istringstream,
// а затем уже из него читаем элементы вектора.
// Прочитать вектор из потока.
template <class X>
auto read_vector(istream &is)
{
vector<X> data;
for (X x; is >> x;)
data.push_back(x);
return data;
}
// Прочитать набор векторов из потока.
template <class X>
auto read_vectors(istream &is)
{
vector<vector<X>> data;
istringstream line_input;
for (string line; getline(is, line);)
{
line_input.str(line); // читать из строки line.
data.push_back(read_vector<X>(line_input));
}
return data;
}
В случае записи “поверх” уже записанных данных потоки вывода (и ostringstream и ofstream) просто затирают символы на позиции записи. Вставка символов или удаление “хвоста” не осуществляются.
#include <iostream>
#include <sstream>
int main()
{
using namespace std;
ostringstream s;
s << "abcdefg";
s.seekp(0);
s.put('x');
cout << s.str() << endl; // > xbcdefg
return 0;
}
Тем не менее, при выводе текста в консоль обычно можно имитировать удаление символов с помощью вывода символа BS
. Например “крутящаяся палочка”:
while (true) std::cout
.put('|').flush().put('\b')
.put('/').flush().put('\b')
.put('-').flush().put('\b')
.put('\\').flush().put('\b');
Впрочем, данное действие довольно бессмысленно. Если мы хотим показать, что идёт некий процесс, можно выполнять небольшую часть работы между сменами знака “палочки”. В примере ниже функция do_next_job выполняет некоторый фрагмент задачи и возвращает true, если надо продолжить ожидание, и false, если работа завершена:
static char sprite[] = "|/-\\";
for (unsigned s = 0;; s = (s + 1) % 4)
{
std::cout.put(sprite[s]).flush();
if (!do_next_job())
break;
std::cout.put('\b');
}
Существует набор стандартных типов, механически сочетающих в себе интерфейсы потока ввода istream и потока вывода ostream. В частности, они предоставляют две независимых позиции: позицию чтения и позицию записи. С их помощью можно работать с файлом (тип fstream
) или строкой (тип stringstream
) как с расширяемым массивом байт, который можно и читать и изменять.
Пример использования stringstream для тестирования функции, читающей данные из потока в примере 0520-euclid_norm_2.cpp:
bool test_euclid_norm_stream()
{
stringstream ts;
ts << 4 << ' ' << 1 << ' ' << 0 << ' ' << 3 << ' ';
ts << 0 << ' ' << -1 << ' ' << -2 << ' ' << -6;
if (!almost_equal(euclid_norm(ts), 8.18535277))
return false;
ts.clear(); // сбросить флаг конца файла
ts.seekp(0); // сбросить позицию записи в поток на начало
ts.seekg(0); // сбросить позицию чтения из потока на начало
ts << -40 << ' ' << -2 << ' ' << -111 << ' ' << 42 << ' ';
ts << 2 << ' ' << -1000 << ' ' << -4;
if (!almost_equal(euclid_norm(ts), 1007.823893))
return false;
// Все тесты прошли успешно.
return true;
}
Кувшинов Д.Р. © 2015