§ 9.8. Текстовые файлы

Школьный курс python
Содержание

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

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

Последовательности для указания конца строки в разных системах отличаются. В UNIX-подобных ОС используется LF (U+000A), от англ. line feed — подача строки, в системах Mac OS до версии 9, OS-9 и др. используется CR (U+000D), от англ. carriage return — возврат каретки, а в MS-DOS, OS/2, Microsoft Windows, Symbian OS и др., а также в протоколах Интернет, используются обе последовательности – CR+LF (U+000D U+000A).

Это позволяет отвлечься от двоичного представления данных в файле и рассматривать файл, как поток символов, аналогичный стандартному (консольному) потоку. Такие потоки называются файловыми потоками ввода/вывода. Для организации файловых потоков используются специальные классы файловых потоков:

  • ifstream – чтения файла (файловый ввод);
  • ofstream – записи в файл (файловый вывод);
  • fstream – чтения и записи в файл.
Они становятся доступными, если включен заголовочный файл:

include <fstream>

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

Файловый поток вывода (вывод в файл). ofstream

Чтобы создать поток вывода, т. е. записи, в файл, необходимо использовать класс ofstream. Для того, чтобы записать что-либо в файл его нужно создать. Файл можно создать с помощью IDE и автоматически (файл, уже существующий в системе, можно добавить в проект позднее). Покажем как это можно сделать в IDE Qt-Creator.
Выполните: Файл > Создать файл или проект… (Ctrl+N). Выберите шаблон: Файлы и классы: Общее > Пустой файл > Выбрать > Далее. В следующем окне укажите имя файла и путь:
В панели “проекты” появится список “другие файлы” с именем файла, включенного в проект.
Класс ofstream может создавать файлы автоматически, если конструктору класса передать параметр-строку – путь к файлу:

ofstream("output");
Ресурсы проекта перечисляются в файле описания проекта с расширением .pro. Здесь же указывается и местоположение добавляемых в проект файлов:

TEMPLATE = app
CONFIG += console c++20
CONFIG -= app_bundle
CONFIG -= qt

SOURCES += \
        main.cpp

DISTFILES += \
        /полный/путь/к/файлу/input \
        /полный/путь/к/файлу/output

Если путь к файлам изменился, то вы можете изменить путь в секции DISTFILES

Обращаем внимание, что путь к файлу должен быть доступен текущему пользователю. В данном случае, путь – относительный, следовательно, файл output будет создан в корне директории проекта.

Если используется полный путь, то нужно иметь в виду, что в Windows разделитель директорий \ (бэк-слэш), а это в C++ – специальный символ, который требует экранирования дополнительным бэк-слэшем (т. е., \\), например, "C:\\dir1\\dir2".

Однако, в программах может использоваться несколько файлов как для ввода, так и для вывода, поэтому имя файла целесообразно указывать при создании потокового объекта, либо в потоковом методе open.
В отличие от консольного потока вывода, объект которого определен как cout, файловый потоковый объект разработчик должен создавать самостоятельно, придерживаясь известных правил использования идентификаторов. В задачах ниже, для определения файлового объекта вывода, используется идентификатор fout. Для создания файлового объекта применяется метод open(). Его можно использовать как явно, так и неявно.
Рассмотрим оба варианта на примере следующей задачи.
Задача 1. Создать файл и записать в файл строку введенную с клавиатуры.

Программа 9.8.1.1
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    string S;
    getline(cin, S);
    ofstream fout("output");
    fout << S << endl;
    fout.close();
    return 0;
}

Если с одним и тем же потоком необходимо связывать разные файлы, то, после создания потокового объекта, начать работу с соответствующим файлом можно с помощью метода open. Тогда код в программе выше нужно переписать следующим образом:

Программа 9.8.1.2
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    string S;
    getline(cin, S);
    ofstream fout;
    fout.open("output");
    fout << S << endl;
    fout.close();
    return 0;
}

Обратите внимание, что файл, открытый методом open, должен быть закрыт методом close. Метод close разрывает связь с файлом (освобождая ресурсы), но не закрывает созданный ранее поток (fout), который может быть связан в программе (с помощью метода open) с другим файлом.
В первом примере функция open будет вызываться неявно. Такой лаконичный кодинг нужен в том случае, когда доступ к файлу в программе выполняется единожды, а не многократно.

Файловый поток ввода (ввод из файла). ifstream

Аналогичным образом построена работа с вводом из файла (чтением файла). В отличие от вывода в файл (который может быть создан автоматически), файл, из которого осуществляется чтение, должен существовать, в противном случае программа завершится выводом ошибки. Для создания потокового объекта ввода используется класс ifstream.
Задача 2. Допустим, в корне проекта существует файл input из которого будет осуществляться чтение. В первой строке этого файла записаны два числа. Необходимо открыть файл для чтения, получить эти два числа, вычислить сумму этих чисел и вывести эту сумму на экран.

Программа 9.8.2
#include <iostream>
#include <fstream>
using namespace std;

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

Для вывода результата в другой файл необходимо создать одновременно и поток вывода, и поток ввода. Такая программа, по аналогичному условию задачи, может быть написана следующим образом.

Программа 9.8.3
#include <iostream>
#include <fstream>
using namespace std;

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

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

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

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

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

  • ios::out | ios::app – открыть для записи с разрешением только на добавление
  • ios::in | ios::out – открыть для чтения и записи с разрешением на запись в произвольном месте файла
  • ios::in | ios::out | ios::trunc – открыть для чтения и записи с усечением существующего файла
Рассмотрим пример задачи в которой применяются режимы работы с файлом.
Задача 3. Записать в исходный файл input 10 строк в каждой из которых находится случайное число из отрезка [10, 99]. Получить из этого файла данные чтением построчно с суммированием и отправить среднее арифметическое в файл output. (Представить вещественное число в экспоненциальном формате).

Программа 9.8.4
#include <fstream>
#include <iostream>
#include <random>
#include <chrono>
using namespace std;
using namespace chrono;

int main() {
    int seed = system_clock::now().time_since_epoch().count();
    default_random_engine rnd(seed);
    uniform_int_distribution<int> d(10, 99);
    int k, j = 0;
    // открыть файл input и для вывода
    fstream fin("input", ios::out);
    for (int i = 1; i <= 10; i++){
        fin << d(rnd) << endl;
    }
    fin.close();
    // теперь открываем файл input для ввода
    fin.open("input", ios::in);
    while (fin >> k) j += k;
    fin.close();    
    ofstream fout("output");
    fout << scientific << float(j) / 10 << endl;
    fout.close();
    return 0;
}
Нельзя открывать бинарные файлы (такие как jpeg, exe, doc) в текстовом режиме! Это может привести к тому, что файлы будут испорчены.

Построчное чтение из файла

Для организации чтения всех существующих строк в файле необходимо использовать цикл while в сочетании с потоковой операцией извлечения (">>"). Также, для определения конца файла, можно применять логическую функцию eof() в сочетании с операцией "!". Но нужно отличать работу eof() в C++, от работы подобных функций в других языках (например, Pascal). В C++ метод eof() возвращает true, если установлен флаг состояния потока (а не прочитана последняя строка файла!). Если в цикле чтение последней строки файла было успешным, то eof() не изменяет своего значения (false на true) и цикл выполняет еще один шаг (что может, в некоторых программах, привести к ошибке) и только после того, как в файле данные не были обнаружены, eof() принимает значение true.
Приведем наглядный пример.
Задача 4. Допустим, у нас есть файл input, в котором записаны числа от 1 до 10. Все числа записаны в отдельных строках. Вывести все строки файла на экран.

Программа 9.8.5.1
#include <iostream>
#include <fstream>
using namespace std;

int main() {
	int i(0);
	int k;
    ifstream fin("input");
    while (!fin.eof()) {
        fin >> k;
        cout << k << endl;
        ++i;
    }
    fin.close();
    cout << "i = " << i << endl;
    return 0;
}

Вывод

1
2
3
4
5
6
7
8
9
10
10
i = 11

Как и следовало ожидать, цикл выполнил 11 шагов. Как проконтролировать: действительно ли все данные в файле прочитаны? Это выполняется путем добавления инструкции if с инструкцией break в тело цикла. В качестве выражения-условия можно использовать функцию fail() (но это не единственный вариант), которая возвращает true, если полученные данные не являются целочисленными.

Программа 9.8.5.2
#include <iostream>
#include <fstream>
using namespace std;

int main() {
	int i(0);
	int k;
    ifstream fin("input");
    while (!fin.eof()) {
        fin >> k;
        if (fin.fail()) break;
        cout << k << endl;
        ++i;
    }
    fin.close();
    cout << "i = " << i << endl;
    return 0;
}

Вывод

1
2
3
4
5
6
7
8
9
10
i = 10

Теперь верно! На последнем шаге цикл прервал свою работу.
Но на практике поступают иначе (см. программу 9.8.4). Чтобы не использовать дополнительную проверку, операцию извлечения помещают непосредственно в условие цикла. Таким образом, наличие читаемых данных в потоке будет проверяться до выполнения шага цикла. Вот как будет выглядеть этот вариант решения задачи 4:

Программа 9.8.5.3
#include <iostream>
#include <fstream>
using namespace std;

int main() {
	int i(0);
	int k;
    ifstream fin("input");
    while (fin >> k) {
        cout << k << endl;
        ++i;
    }
    fin.close();
    cout << "i = " << i << endl;
    return 0;
}

Если необходимо проанализировать каждую строку в файле, то организуется структура вложенных циклов. Во внешнем цикле перебираются все строки файла (как в примере выше), а во вложенном – решается поставленная задача (разбирается строка).
Обратите внимание, что при каждом обращении к fin в цикле, из потока будет извлекаться массив символов (один или более) до следующего пробельного символа. Таким образом, чтобы получать строку файла целиком (с переходом к следующей) необходимо использовать функцию getline() и далее работать с символьным массивом. Это уменьшит эффективность программы, так как в дальнейшем необходимо обрабатывать саму строку, например, получать из нее числовой массив. Можно ли получить такой массив непосредственно при вводе? Естественно, можно!

У каждого файлового объекта есть два ассоциированных с ним значения, называемые указатели (или, лучше сказать, “курсоры“) чтения (get) и записи (set). Их значения определяют номер байта относительно начала файла, с которого будет производиться чтение или запись. Классы файловых потоков имеют функции управления этими указателями (здесь не рассматриваются). При выполнении операций вставки (<<) и извлечения (>>) значения указателей будут увеличиваться. Следовательно, если из файла нужно получить значения (разделенные пробелом) для некоторых переменных, нужно всего лишь выполнить последовательно несколько операций извлечения для получения этих значений.

Для решения такой задачи нам необходимо использовать стандартные потоковые функции. Рассмотрим пример задачи, где реализован такой подход (числовой массив здесь не получаем, но могли бы).
Задача 5. Дан файл input. В каждой строке этого файла записан массив целых чисел (разделенных одним пробелом) равной длины. Определить и вывести на экран сумму чисел в каждом массиве (т. е. по каждой строке).

Программа 9.8.6
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    ifstream fin("input");
    while (!fin.eof()) {
        int s = 0;
        while (fin.peek() != '\n') {
            int k;
            fin >> k;
            s += k;
        }
        fin.ignore(2);
        cout << s << endl;
    }
    fin.close();
    return 0;
}

Файл input

1 2 3 4 5 6 7 8 9
2 3 4 5 6 7 8 9 0
3 4 5 6 7 8 9 0 1
4 5 6 7 8 9 0 1 2

Вывод

45
42
40
38

Заметим, что этим способом можно решать задачи и с файлами, в которых строки имеют переменную длину.
В этой программе используются два метода класса fstream:

  • peek() – читает следующий символ из потока без его извлечения;
  • ignore(count) – извлекает и отбрасывает count символов из потока
Первая функция нужна для проверки символа, который потенциально мог бы быть извлеченным из потока. Таким образом определяется признак конца строки. Вторая функция нужна чтобы отбросить символы признака конца строки и перевода строки на новую. В данной программе нет необходимости дополнительно проверять наличие данных в потоке.

Строковые потоки

Для решения задач, в которых строки файла подвергаются некоторому анализу, есть более "элегантный" подход - это организация строкового потока. Строковые потоки осуществляют ввод/вывод в оперативной памяти и используют буфер для записи в строку и чтения из строки, как стандартные или файловые потоки. Заголовок sstream определяет три класса строковых потоков:

  • istringstream – Строковый ввод
  • ostringstream – Строковый вывод
  • stringstream – Строковый ввод и вывод
Рассмотрим применение входного строкового потока.
Задача 6. В файле input записаны несколько строк. Определить номер строки в которой находится самое длинное слово.

Программа 9.8.7
#include <iostream>
#include <fstream>
#include <sstream>
using namespace std;

int main() {
	ifstream fin("input");
	unsigned Max = 0, i, k = 0;
	while (!fin.eof()) {
		++k;
		string line, word;
		getline(fin, line);
		// Создаем из строки строковый поток
		istringstream is(line);
		// Работаем с каждым словом по отдельности
		while (is) {
			is >> word;
			if (word.size() > Max) {
					Max = word.size();
					i = k;
			}
		}
	}
	cout << "Самое большое слово состоит из " << Max << " букв\n"
		 << "находится на " << i << " строке." << endl;
	fin.close();
	return 0;
}

Файл input

qqq qqqqqq qqqqqqqqqqqqqqq
q q q
qqq qqq qqq
q qqq q qqq
qqqqqqqqq qqqqqqqqq

Вывод

Самое большое слово состоит из 15 букв
находится на 1 строке.
Метод str()

Для строковых потоков описан специальный метод str(). Этот метод недоступен в других потоковых классах. Он выполняет двоякую роль. Если функция не имеет аргументов (как в программе, ниже), то возвращается копия строки, которую хранит потоковый объект вывода (os). Если функция имеет аргумент (которым является передаваемая строка), то аргумент будет копироваться в потоковый объект.
Рассмотрим пример задачи, в которой применяется как входной, так и выходной строковые потоки.
Задача 7. В файле input записана строка, в которой слова разделены пробелами (пробелы не повторяются). Получить файл output в котором записана новая строка, слова которой разделены символом '.' (точка). В конце строки точки не должно быть.

Программа 9.8.8
#include <iostream>
#include <fstream>
#include <sstream>
using namespace std;

int main() {
	ifstream fin("input");
	ofstream fout("output");
	string line, word;
	getline(fin, line);
	istringstream is(line);
	ostringstream os;
	while (is) {
		is >> word;
		is.ignore();
		os << (!is ? word : word + ".");
	}
	fout << os.str() << endl;
	fin.close();
	fout.close();
	return 0;
}

Файл input

q qq qqq qqqq qqqqq qqqqqq

Файл output

q.qq.qqq.qqqq.qqqqq.qqqqqq

Для того, чтобы завершающий (нулевой) символ не воспринимался как еще одно слово, мы воспользовались, знакомым уже нам, методом ignore().

Приложение

Примеры решения задач
Задача 8. Текстовый файл состоит не более чем из 1'200'000 символов X, Y, и Z. Определите максимальное количество идущих подряд символов, среди которых нет подстроки XZZY. (Открытый вариант КЕГЭ-2021, зад. 24).
Файл к задаче.

Программа 9.8.9
#include <iostream>
#include <fstream>
#include <sstream>
using namespace std;

int main() {
	ifstream fin("24.txt");
	string S, word;
	string sub = "XZZY";
	getline(fin, S);
	// Заменим "XZZY" на ' '
	size_t pos {}, Max {};
	while (S.find(sub, pos) != string::npos){
		pos = S.find(sub, pos);
		S.replace(pos, sub.size(), " ");
	}
	// Создаем поток
	istringstream is(S);
	while (is) {
		is >> word;
		if (word.size() > Max)
			Max = word.size();
	}
	cout << Max << endl;
	fin.close();
	return 0;
}

Вывод

1707

Задача 9. На грузовом судне необходимо перевезти контейнеры, имеющие одинаковый габарит и разные массы. Общая масса всех контейнеров превышает грузоподъёмность судна. Количество грузовых мест на судне не меньше количества контейнеров, назначенных к перевозке. Какое максимальное количество контейнеров можно перевезти за один рейс и какова масса самого тяжёлого контейнера среди всех контейнеров, которые можно перевезти за один рейс?
Входные данные. В первой строке входного файла находятся два числа: S – грузоподъёмность судна (натуральное число, не превышающее 100'000) и N – количество контейнеров (натуральное число, не превышающее 10'000). В следующих N строках находятся значения масс контейнеров, требующих транспортировки (все числа натуральные, не превышающие 100), каждое в отдельной строке.
Выходные данные. Два целых неотрицательных числа: максимальное количество контейнеров, которые можно перевезти за один рейс и масса наиболее тяжёлого из них. (Открытый вариант КЕГЭ-2021, зад. 26).
Файл к задаче

Программа 9.8.10
#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
	ifstream fin("26.txt");
	int S, N;
	fin >> S >> N;
	vector<int> ar;
	while (fin >> N)
		ar.push_back(N);
	// Сортируем по возрастанию
	sort(ar.begin(), ar.end());
	int Sum {}, i {};
	while (Sum + ar[i] <= S) {
		Sum += ar[i];
		++i;
	}
	fin.close();
	cout << i << " " << ar[i-1] << endl;
	return 0;
}
1612 65
Вопросы
Темы сообщений
Задания А
Задания Б
Задания С
Ссылки
1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 5,00 из 5)
Загрузка...

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.


Обсуждение закрыто.