§21 Файловые потоки. Потоковые итераторы

Классы файловых потоков

После окончания работы программы и вывода результатов на дисплей данные будут утрачены безвозвратно, если они не будут записаны и сохранены в долговременной памяти. Для сохранения данных в долговременной памяти используются файлы. Файл (от англ. file) — это именованная область памяти на носителе информации. В программировании различают два типа файлов: текстовые и двоичные (бинарные). Вне зависимости от организации данных в файлах, данные в них представлены в двоичном формате, так что это деление условное. В текстовых файлах данные интерпретируются как последовательность символьных кодов. Специальные последовательности используются для указания признака конца строки. Это позволяет отвлечься от двоичного представления и рассматривать файл, как поток символов, равнозначный консольному потоку. Следовательно, для организации работы с текстовым файлом создаются потоки, аналогичные стандартным. Для работы с файлами существуют специальные классы файловых потоков ввода/вывода.
Для того, чтобы начать работу с файлом необходимо подключить заголовочный файл <fstream>.
В этом заголовочном файле определены три класса потоков:

  • ifstream – чтения файла (файловый ввод);
  • ofstream – записи в файл (файловый вывод);
  • fstream – чтение и запись.

Файловые потоки наследуют из базовых классов istream и ostream общие для всех потоков низкоуровневые функции неформатированного ввода/вывода. Некоторые из этих методов мы использовали довольно часто (get(), getline(), ignore()).
Программа 21.1 Произвести посимвольный вывод в файл. Произвести чтение из файла блоком и, также, блоком вывести информацию, содержащуюся в файле на дисплей.

#include <iostream>
#include <fstream>
using namespace std;

int main() {
	fstream fio("output", ios::in | ios::out);
	char C = 'A';
	for (int i = 0; i < 11; i++) {
		for (int j = 0; j < i; j++)
			fio.put(C);
		fio.put('\n');
	}
	fio.seekg(1, ios::beg);
	char S[100];
	fio.read(S, 100);
	cout.write(S, 65);
	return 0;
}

Альтернативой применению этих методов являются потоковые итераторы, которые мы рассмотрим на этом уроке.
Из базового класса ios наследуются также манипуляторы, функции управляющие флагами форматирования и состояния (последние мы обсудим позднее, при знакомстве с обработкой ошибок в C++) и многие другие функции, используемые в классах потоков, рассмотрение которых выходит за рамки нашего курса. Работа с разными потоками, как мы уже говорили, достаточно унифицирована. В этом мы ещё раз убедимся при рассмотрении файловых потоков.

Создание объекта файлового потока ввода

Создание объекта файлового ввода аналогичен созданию объекта строкового ввода, рассмотренного нами ранее. Для создания объекта файлового потока ввода необходимо использовать имя класса и имя потока (конструктор), а также метод open() связывающую потоковый объект с конкретным файлом.
Пример создания объекта fin (имя объекта выбирается произвольно, как любой другой пользовательский идентификатор):

ifstream fin;
fin.open("input");

Метод open() принимает в качестве аргумента строку – имя файла (input). Предполагается, что файл находится в текущей папке проекта (иначе необходимо указать путь к файлу). Файл должен существовать и он должен быть доступен для чтения в системе!
Код создания объекта можно сократить до следующего вида:

ifstream fin("input");

После создания объекта ввода, его можно использовать в программе точно так же, как и объект cin. Например:

fin >> a >> b >> c;

По завершении работы с файлом соединение с ним должно быть разорвано вызовом метода close:

fin.close();

Создание объекта файлового потока вывода

Для создания объекта потока файлового вывода необходимо выполнить следующее:

ofstream fout;
fout.open("output");
или
ofstream fout("output");

Если файла output не существует – он будет создан в текущей директории, в противном случае информация в файле будет стерта.
После создания объекта вывода, его можно использовать в программе точно так же, как и объект cout. Например:

fout << a << b << c;

По завершении работы с файлом соединение с ним должно быть разорвано вызовом метода close:

fout.close();

Примечание. Метод close прекращает работу с файлом и (неявно) уничтожает потоковый объект, но не сами потоки. Потоки fin и fout можно, впоследствии, вновь связать с этими или же с другими файлами.
По умолчанию, файл открывается в текстовом режиме.
Рассмотрим пример. Создадим текстовый файл с именем input в папке нового проекта. Запишем в первой строке этого файла два целых числа разделенных пробелом и сохраним изменения.
Программа 21.2 В исходном файле (input) записаны два числа, разделенные пробелом. Получить сумму этих двух чисел и записать сумму в выходном файле (output)

#include <fstream>
#include <iostream>
using namespace std;

int main() {
	int a, b;
	ifstream fin("input");
	ofstream fout("output");
	fin >> a >> b;
	cout << a << " " << b << endl;
	fout << a + b << endl;
	fin.close();
	fout.close();
	return 0;
}

Если файл будет открыт успешно, то в терминальном окне вы увидите два числа, которые мы записали во входном файле. Откроем папку проекта. Если работа программы завершилась без ошибок, то в этой же папке мы увидим и другой - выходной файл (output) с суммой этих двух чисел. Для удобства работы добавьте эти два файла в проект: Project → Add files

codebl1

Указатели чтения и записи

У каждого файлового объекта есть два ассоциированных с ним значения, называемые указатель чтения (get) и указатель записи (set). Эти значения определяют номер байта относительно начала файла, с которого будет производиться чтение или запись. Классы ifstream и ofstream имеют свои функции для перемещения указателя и определения его текущей позиции.

ifstream		ofstream
seekg(new_position)	seekp(new_position)
seekg(offset, from)	seekp(offset, from)
tellg()			tellp()

При чтении или записи в файл указатели перемещаются по файлу автоматически, но, при необходимости, указатели можно перемещать на заданную позицию. Методы seekg и seekp осуществляют смещение указателя от текущей позиции на величину new_position или offset. Аргументы new_position и offset (позиция файла и смещение от этой позиции) имеют машинно-зависимый целочисленный тип. Для offset он может быть как положительным, так и отрицательным (соответственно вперед и назад). Аргумент from - имеет три возможных значения:

  • ios::beg - отсчет смещения от начала
  • ios::cur - отсчет от текущей позиции
  • ios::end - отсчет от конца файла

Например:

  • fin.seekg(2, ios::beg); - 2 байта от начала
  • fin.seekg(-1, ios::cur); - назад на 1 байт
  • fin.seekg(0, ios::end); - перейти в конец файла

Если нужно получить текущее значение позиции указателя, то необходимо воспользоваться методами tellg() или tellp().
Примечание. В некоторых олимпиадных заданиях или заданиях ЕГЭ в первой строке файла подается число, которое, в большинстве случаев, предназначено для организации циклов. Но, если вы организуете цикл другими способами (например, с помощью итераторов), то используя fin.seekg(2, ios::beg); можно обойти это число, чтобы перевести указатель на следующую строку.
Метод eof() позволяет проверить находится ли указатель в конце файла. Данный метод возвращает 0, если конец файла ещё не достигнут и 1, в противном случае. Таким образом, можно применять eof() для осуществления построчного чтения текстового файла, например, в цикле while.

Режимы работы с файлом

Конструкторы ifstream и ofstream, а также метод open(), принимают, на самом деле, два аргумента. Второй аргумент определяет режим работы с файлом. Отсутствие второго аргумента означает, что второй аргумент задан не явно (по умолчанию). Режимы работы с файлом определяют каким образом нужно осуществлять работу с файлом. Ниже перечислены эти режимы:

  • ios::in - открыть файл для ввода (чтения). Может быть установлен только для объектов типа ifstream (режим по умолчанию) и fstream
  • ios::out - открыть файл для вывода (записи). Может быть установлен только для объектов типа ofstream (режим по умолчанию) и fstream
  • ios::ate - перейти в конец файла после открытия. Позиция указателя может быть изменена
  • ios::app - открыть файл для записи в конец файла. Может быть установлен, если не установлен режим trunc. Если режим app установлен, файл всегда открывается в режиме вывода (записи)
  • ios::trunc - усекает (удаляет из него всю информацию) файл, если он существует. Может быть установлен, только если устанавливается также режим out (когда используется конструктор fstream)
  • ios::binary - открыть в двоичном режиме

Битовая операция "|" служит для объединения нескольких режимов. В классе fstream режим по умолчанию не предусмотрен, поэтому при создании объектов данного класса режим нужно устанавливать явно.
Примеры использования:

  • ios::out | ios::app - открыть для записи с разрешением только на добавление
  • ios::in | ios::out - открыть для чтения и записи с разрешением на запись в произвольном месте файла
  • ios::in | ios::out | ios::trunc - открыть для чтения и записи с усечением существующего файла

Рассмотрим пример задачи в которой применяются как разные режимы, так и функции перемещения файлового указателя.
Программа 21.3 Записать в исходный файл input 10 строк в каждой из которых находится случайное число из отрезка [10, 99]. Получить из этого файла данные чтением построчно с суммированием и отправить среднее арифметическое в файл output. (Представить вещественное число в экспоненциальном формате).

#include <fstream>
#include <iostream>
#include <iomanip>
#include <ctime>
#include <cstdlib>
using namespace std;

int main() {
	srand(time(0));
	int k = 0, j = 0;
	// открыть файл и для вывода, и для ввода
	fstream fin("input", ios::out | ios::in);
	ofstream fout("output");
	for (int i = 1; i <= 10; i++){
		k = 10 + rand() % 90;
		fin << k << endl;
	}
	// вернем указатель в начало файла
	fin.seekg(0, ios::beg);
	while (!fin.eof()){
		fin >> k;
		j += k;
	}
	fout << scientific << float(j) / 10 << endl;
	fin.close();
	fout.close();
	return 0;
}

Файловые и строковые потоки

Для организации посимвольного чтения из файла и записи в файл применяются, перечисленные в начале урока, методы неформатированного ввода и вывода. Однако, часто возникает ситуация, когда необходимо получать строки целиком, а затем производить словарный анализ. Для этих целей используется, уже хорошо известная нам, функция getline класса string и строковый поток, который создается на основе извлеченной строки. Приведем пример задачи и разберем ее более детально, поскольку использование такого подхода в работе с данными требуется довольно часто.
Программа 21.4 В файле input в первой строке записан IP-адрес сети и маска, далее в каждой отдельной строке записываются:
Логин Имя IP-адрес_компьютера
Вывести на дисплей, упорядоченные в алфовитном порядке, логины тех пользователей, чьи компьютеры находятся в данной сети.

#include <iostream>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <array>
#include <string>
#include <vector>
using namespace std;

template<typename T1, typename T2>
void addip(T1&, T2&);

int main() {
	// Массивы для сохранения отдельных байтов (октетов)
	// IP-адреса и маски
	array<int, 4> ip;
	array<int, 4> mask;
	vector<string> users;
	string S1, S2;
	//===================================================//
	// Создаем файловый поток                            //
	//===================================================//
	ifstream fin("input");
	fin >> S1 >> S2;
	fin.ignore();
	addip(ip, S1);
	addip(mask, S2);
	while (!fin.eof()) {
		array<int, 4> net;
		string login, name, user_ip, S;
		getline(fin, S);
		//===============================================//
		// Создаем строковый поток                       //
		//===============================================//
		istringstream is(S);
		is >> login >> name >> user_ip;
		addip(net, user_ip);
		// Выполняя поразрядную конъюнкцию IP-адреса и
		// маски мы получаем IP-адрес сети, который
		// проверяется с уже хранящемся в массиве ip
		for (size_t i = 0; i < net.size(); i++)
			net[i] &= mask[i];
		if (net == ip)
			users.push_back(login);
	}
	sort(users.begin(), users.end());
	for (auto &r : users)
		cout << r << endl;
	return 0;
}

//===================================================//
// Шаблонная функция извлечения из строки октетов    //
// с записью их в массиве                            //
//===================================================//
template<typename T1, typename T2>
void addip(T1 &ar, T2 &St) {
	St += '.';
	for (auto &r : ar) {
		size_t k = St.find('.');
		T2 S = St.substr(0, k);
		St.erase(0, k + 1);
		r = stoi(S);
	}
};

Потоковые итераторы

Возможно, наш рассказ о потоках будет не полным, если мы не познакомимся с потоковыми итераторами (stream iterator). По существу, потоковые итераторы - это адаптеры итераторов, которые совместимы с функциями библиотеки algorithm. Для начала работы с потоковыми итераторами необходимо включить заголовок:

#include <iterator>
istream_iterator
istreambuf_iterator

Потоковый итератор ввода (istream_iterator) является однопроходным итератором, который производит последовательное чтение объектов типа T из объекта basic_istream. Работа итератора выполняется следующим образом: вначале производится инициализация итератора соответствующим потоком ввода. Затем, путем вызова соответствующей операции operator>>, производится последовательное чтение объектов потока. Следует иметь ввиду, что операция чтения выполняется при увеличении итератора, а не при его разыменовании. Однако, хотя операция it++ применима к потоковым итераторам, на самом деле является фиктивной (так что её можно опустить или использовать для декорации).
Программа 21.5 Инициализируйте вектор потоковыми итераторами непосредственно и по критерию.

#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std;

int main() {
	// На основе строки
	string S("1 2 3 4 5 6 7 8 9 10 11 12 13 14 15");
	// Создаем строковый поток
	istringstream is(S);
	// Этим потоком инициализируем итераторы
	// Итератор начала потока
	istream_iterator<int> is_first(is);
	// Итератор конца; обратите внимание на отсутствие скобок
	istream_iterator<int> is_last;
	// А потоковыми итераторами инициализируем вектор
	vector<int> V(is_first, is_last);
	for (auto &r : V)
		cout << r << " ";
	cout << endl;
	// Сброс флагов состояния, иначе невозможно возобновить поток 
	is.clear();
	// Вновь отправляем эту же строку в поток
	is.str(S);
	// Более компактное описание итераторов
	// eof, в данном случае, имя итератора, а не функция
	istream_iterator<int> is_it(is), eof;
	vector<int> Va(15);
	// Заполним вектор из потока по критерию
	copy_if(is_it, eof, Va.begin(), [](const int &r){return !(r % 3);});
	for (auto &r : Va)
		cout << r << " ";
	cout << endl;
	return 0;
}

Примечание. Потоковые итераторы необходимо определять в месте их использования.
Разновидностью потоковых итераторов являются итераторы потоковых буферов. Определены они в том же заголовке. В частности, для входного потока используется istreambuf_iterator. Они отличаются от обычной формы лишь в том, что элементами потока являются символы (указание типа данных обязательно). Работа этих итераторов на символах более эффективна, так как позволяет избежать накладных расходов по созданию и разрушению временных объектов (других типов).
Программа 21.6 Скопировать из потока в строку только уникальные символы.

#include <iostream>
#include <string>
#include <sstream>
#include <iterator>
#include <algorithm>
using namespace std;

int main() {
	istringstream is("qqqqqqwwwwweeerrrrrtyu");
	istreambuf_iterator<char> in_it(is), it_eof;
	string S;
	// Обязательно устанавливаем размер строки,
	// иначе некуда будет копировать
	S.resize(20);
	// Копируем в строку только уникальные символы
	unique_copy(in_it, it_eof, S.begin());
	cout << S << endl;
	return 0;
}
ostream_iterator
ostreambuf_iterator

Итераторы потоков вывода работают аналогично. Конструктор итератора ostream_iterator может иметь (не обязательно) строку разделитель. Эта строка будет добавлена в поток, в самом конце. Продемонстрируем на примере.
Программа 21.7 С помощью потокового итератора вывода обеспечить вывод числовой последовательности в файл и на дисплей. С помощью алгоритма copy вывести содержимое вектора на дисплей.

#include <iostream>
#include <string>
#include <fstream>
#include <iterator>
#include <algorithm>
#include <vector>
using namespace std;

int main() {
	int i = 20;
	ostream_iterator<int> os_it(cout, " ");
	// Помещаем числа в стандартный поток
	// используя интерфейс итераторов
	while (--i) {
		*os_it = i;
		 // Да, но можно опустить
		 os_it++;
	}
	// Создаем файловый поток вывода
	ofstream fout("output");
	// А теперь запишем в файл несколько таких строк
	ostream_iterator<int> of_it(fout, " ");
	i = 10;
	while (--i) {
		int j = 20;
		while (--j) {
			*of_it = j;
			 of_it++;
		}
		fout << '\n';
	}
	cout << endl;
	// Выведем на экран содержимое вектора
	vector<int> mas {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
	copy(mas.cbegin(), mas.cend(), ostream_iterator<int>(cout, " "));
	return 0;
}


Print Friendly, PDF & Email

Добавить комментарий