Строки и потоки ввода-вывода C++

Кувшинов Д.Р.

2015


Общее оглавление


Кодировки

Кодировка символов — способ записи последовательности символов (строки) с помощью последовательности натуральных чисел (“кодов”). В простейшем случае кодировка задаётся таблицей символов, в которой перечислены все доступные символы, и каждому из них присвоен код (номер). Как правило, в более сложных случаях такая таблица всё равно есть, но кодировка включает в себя особенности оформления кодов с целью уменьшения размера сообщения в байтах.

ASCII

ASCII (от American Standard Code for Information Interchange — “американский стандартный код для обмена информацией”) — кодировка символов, получившая широкое распространение.

Кодировка ASCII содержит 128 символов, каждому из которых соответствует своя последовательность семи бит (двоичная запись номера символа).

Управляющие символы

Первые 32 кода и последний (символ 127) выделены для специальных управляющих символов. Большинство этих кодов были важны во времена использования телетайпов и других подобных устройств, напрямую управлявшихся последовательностью переданных на них символов, и в настоящее время практически не используются. Символ 127 (DEL) использовался для уничтожения содержимого перфоленты при записи поверх старого содержимого (код 127 соответствует семи пробитым отверстиям).

Некоторые управляющие символы ASCII
Номер Номер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 удалить следующий символ

Замечания

Печатные символы

Полную таблицу см. здесь.

Некоторые печатные символы ASCII
Номера Номера16 Символы Запись в C
32 20 пробел пробел
34 22 двойные кавычки " \"
39 27 кавычка ' \'
48–57 30–39 цифры 0–9 09
65–90 41–5A заглавные латинские буквы A–Z AZ
92 5C обратная косая черта \ \\
95 5F знак подчёркивания _
97–122 61–7A строчные латинские буквы a–z az

Замечания

Восьмибитные кодировки

Один восьмибитный байт может принимать 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++

В языке 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

Си-строка или нуль-терминированная строка — структура данных, представляющая собой массив символов, последний из которых (и только он) является нулевым. Так как нулевой символ маркирует конец си-строки, для передачи её в функцию достаточно передать указатель на её начало. Любой указатель на символ некоторой си-строки является указателем на её подстроку — тоже си-строку.

Заголовочный файл cstring содержит базовый функционал для работы с си-строками и массивами символов (байт). Массивы байт передаются парой (указатель (типа void*), размер (в байтах)). Далее приведены краткие описания некоторых стандартных функций, объявленных в этом заголовочном файле.

Для краткости даны объявления функций только для языка C. В случае C++ нередко существуют аналогичные пары функций с различной константностью (C “теряет” константность), например, в C:

В то же время в C++ (в пространстве имён std) имеется сразу два варианта:

Блоки памяти

Си-строки

Все эти функции порождают неопределённое поведение, если в качестве параметра, подразумевающего передачу адреса си-строки с завершающим нулём, передаётся указатель, не указывающий на си-строку.

Также существуют аналоги данных функций для работы с wchar_t и многобайтными кодировками.

Стандартный заголовочный файл string

Возможности C++ позволяют определить тип данных, объекты которого самостоятельно управляют массивом символов (строкой) и предоставляют набор удобных в использовании базовых операций.

Стандартные строковые типы
Тип строки Тип элемента Стандарт
string char C++98
wstring wchar_t C++98
u16string char16_t C++11
u32string char32_t C++11

Все перечисленные строковые типы по существу устроены одинаково и предоставляют одинаковые с точностью до типа данных наборы операций. Фактически они являются объектами, управляющими динамическими массивами символов, в которых и хранятся строки. В отличие от си-строк, данные объекты позволяют хранить произвольные символы, включая нулевые. Строки можно сравнивать с помощью обычных операций сравнения вроде == и < (используется лексикографический порядок).

Простейшие операции с объектами string

Далее приведены примеры использования некоторых возможностей стандартных строк.

// Переменную строкового типа можно инициализировать литералом соответствующего типа
// или просто нуль-терминированной си-строкой (указателем на 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 также определены удобные функции преобразования между числами и их строковыми представлениями:

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

Элементы библиотеки Iostreams

Базовые элементы

Вывод текстового представления значения 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 символов

К сожалению, такой нехитрый приём не работает в ряде случаев, когда поток ввода содержит неопределённое количество “мусора”. Более универсальный метод представлен в примере по ссылке.

Использование fstream

// Подключить файловые потоки ввода-вывода:
#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. Каждый из них будет читать данные со своей позиции чтения.

Использование sstream

// Подключить потоки вывода в строку.
#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');
}

C++, HTML


iostream-типы

Существует набор стандартных типов, механически сочетающих в себе интерфейсы потока ввода 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