Добавлен: 25.05.2023
Просмотров: 263
Скачиваний: 3
Можно сделать код устойчивее (либеральнее по отношению к вводимым данным), если обрабатывать ситуации, в которых файл содержит меньше десяти элементов. Для этого можно еще раз проверить результат метода, который считывает данные.
#include <fstream>
#include <iostream>
#include <vector>
using namespace std;
int main ()
{
ifstream file_reader("highscores.txt");
if (! file_reader.is_open())
{
cout << “Could not open file!" << '\n';
}
vector<int> scores;
for (int i = 0; i < 10; i++)
{
int score;
if(! (file_reader >> score))
{
breack;
}
scores.push_back(score);
}
}
Обрабатывая файл менее чем с десятью записями, код прекращает чтение, достигнув конца файла. Воспользовавшись вектором вместо массива с фиксированной длиной, мы можем легко обрабатывать короткие файлы. Вектор сохранит ровно столько данных, сколько было считано. Если мы сделаем, то же с массивом, придется отслеживать количество хранимых в нем записей, поскольку мы не знаем, заполнен ли массив целиком.
Иногда требуется считывать из файла все данные. В таких случаях необходимо отличать ошибки чтения, обусловленные достижением конца файла, от ошибок, вызванных некорректностью файла. Метод eof определяет, достигнут ли конец файла. Можно написать цикл, который считывает максимально возможное количество данных и проверяет результат чтения, пока не происходит ошибка. Затем следует проверить значение, возвращаемое методом eof: если оно равно true, находимся в конце файла, в противном случае произошла ошибка. Выяснить причину ошибки можно с помощью метода fail, который возвращает true, если введенные данные некорректны или возникла проблема считывания данных с устройства. После того как конец файла достигнут, нужно вызвать метод clear, чтобы продолжить выполнение операций над файлом [13, с.361][13].
Существует еще одно важное различие между ситуациями, когда данные считываются из файлов и когда их вводит пользователь. Что произойдет, если мы включим в список рекордов не только количество очков, но и имя игрока? Нам потребуется считать из файла как количество очков, так и имя игрока, а для этого необходимо соответствующим образом изменить код. Предыдущие версии программы не смогут считывать файл нового формата. Если много пользователей, изменение формата файла может повлечь серьезные проблемы. Существуют методы обеспечения будущей совместимости файла, которые основаны на использовании в файле дополнительных полей или игнорировании новых элементов формата в устаревших версиях программы. Изучение этих методов не относится к теме этого исследования, просто нужно иметь в виду, что определение формата файла является гораздо важнее определения базового интерфейса [14, с.59][14].
Глава 4. Запись в файлы. Позиция в файле
Для записи в файлы применяется тип ofstream (сокращение от output file stream - исходящий файловый поток). Этот тип почти идентичен типу ifstream с единственным исключением: он используется аналогично не объекту cin, а объекту cout.
Рассмотрим простую программу, которая выводит числа от 0 до 9 в файл highscores.txt (этот код в дальнейшем создаст нам нечто более похожее на таблицу рекордов).
#include <fstream>
#include <iostream>
#include <cstdlib>
using namespace std;
int main ()
{
ofstream file_writer("highscores.txt");
if (! file_writer.is_open())
{
cout << "Could not open file!” << *\n';
return 0;
}
// поскольку у нас нет реальных рекордов, мы выведем
// числа от 10 до 1
for (int i = 0; i < 10; i++)
{
file_writer << 10 - i << '\n';
}
}
Здесь не нужно беспокоиться о том, что мы достигли конца файла. Когда при записи в файл вы попадаете в его конец, объект ofstream расширяет файл. Эта операция называется добавлением в конец файла.
Создание нового файла.
Когда для записи в файл мы используем объект ofstream, по умолчанию он создает этот файл, если он не существует, или перезаписывает содержимое существующего файла. При работе со списком рекордов, скорее всего, перезапись файла при каждом сохранении вполне допустима, поскольку мы возвращаем в него все нужные данные. Тем не менее если мы ведем журнал и регистрируем в нем дату и время каждого запуска программы, периодически перезаписывать его, безусловно, нельзя.
Конструктор типа ofstream принимает второй аргумент, который определяет метод обработки файла (табл. 1):
Таблица 1 – Методы обработки файла
|
Аргумент |
Метод |
|
ios::app |
Добавляет данные в конец файла; после каждой операции записи текущая позиция устанавливается в конец файла |
|
ios::ate |
Устанавливает текущую позицию в конец файла |
|
ios::trunc |
Удаляет все содержимое файла |
|
ios::out |
Разрешает вывод в файл |
|
ios::binary |
Разрешает выполнение двоичных операций над потоком |
Чтобы воспользоваться несколькими методами одновременно, можно объединять их с помощью вертикальной черты (|):
ofstream a_file(“test.txt”, ios::app | ios::binary);
Этот код открывает файл, при этом не уничтожая его текущее содержимое, и позволяет добавлять двоичные данные в конец файла.
Когда программа читает файл или записывает в него, код ввода-вывода должен знать, в каком месте файла следует выполнить соответствующую операцию. Позиция в файле аналогична курсору на экране, который указывает, где будет отображен следующий введенный символ.
При выполнении базовых операций нет необходимости уделять особое внимание позиции в файле: код может просто считывать или записывать очередной фрагмент данных. Тем не менее можно изменять позицию в файле, не выполняя операцию чтения. Это часто необходимо при работе с файлами, которые хранят сложные структуры данных, например, ZIP или PDF, и с файлами, считывание каждого байта из которых занимает много времени или невозможно (например, при реализации базы данных) [11, с.265][15].
Фактически в файле существуют две позиции, одна из которых определяет место для следующей операции чтения, а другая - для следующей операции записи. Узнать текущую позицию для чтения можно с помощью метода tellg (g означает get - получение, чтение данных), а текущую позицию для записи - с помощью метода tellp (р означает put - размещение, запись данных).
Новую позицию в файле также можно задать, перемещаясь от текущей позиции с помощью методов seekp и seekg. Перемещение по файлу называется поиском (это отражено в названиях методов, которые происходят от слова seek - искать). При поиске в файле позиция чтения или записи перемещается на новое место. Оба указанных метода принимают два параметра: расстояние и начало поиска. Расстояние измеряется в байтах, а началом поиска является текущая позиция, начало или конец файла. После поиска мы можем считывать или записывать данные в файл, начиная с новой позиции. Изменение одной позиции с помощью операции поиска не влияет на другую позицию.
Позиция в файле задается с помощью одного из трех флагов (табл. 2):
Таблица 2 – Флаги, задающие позицию в файле
|
Аргумент |
Метод |
|
ios_base::beg |
Поиск от начала файла |
|
ios_base::cur |
Поиск от текущей позиции |
|
ios_base::end |
Поиск от конца файла |
Например, чтобы переместить текущую позицию в начало файла перед тем, как начать запись в него, можно написать следующее:
file_writer.seekp(0, ios_base::beg);
Значение, возвращаемое методами tellp и tellg, представляет собой особый тип переменной - streampos, который определен в стандартной библиотеке. Значение можно преобразовать в целое число и обратно, однако использование streampos явно задает тип данных. Целое число можно использовать где угодно, а у типа streampos есть конкретное назначение. Переменные streampos хранят позиции внутри файлов и используются для установки указателя на эти позиции. Задавая переменным подходящий тип, мы четко указываем их назначение.
streampos pos = file_reader.tellg();
Иногда перемещать текущую позицию в файле не требуется: достаточно просто считать файл от начала до конца. Тем не менее многие форматы файлов оптимизированы для добавления в них новых данных. Новые данные добавляются в конец файла гораздо быстрее, чем в середину. Когда данные добавляются в середину файла, необходимо сдвигать все его содержимое, находящееся после места вставки, так же как при добавлении нового элемента в середину массива.
Дополним написанную ранее программу, которая работает с таблицей рекордов, возможностью добавления нового рекорда в файл. Поскольку для этого необходимо как считывать данные из файла, так и записывать их, мы воспользуемся классом fstream, который поддерживает обе операции (можно считать, что он объединяет возможности классов ofstream и ifstream). Сначала считаем новый рекорд, который вводит пользователь, а затем будем считывать каждую строку файла, пока не найдем рекорд меньше введенного. В этом месте вставим новый рекорд. Сохраним эту позицию и вернемся к ней, считав все оставшиеся строки файла в вектор. Запишем в файл новый рекорд, а затем все остальные рекорды, которые уже были в файле.
int main ()
{
fstream file (
"highscores.txt",
ios::in | ios::out
);
if (! file.is_open())
{
cout << “Could not open file!" << ‘\n’;
return 0;
}
int new_high_score;
cout << “Enter a new high score: ";
сin >> new_high_score;
streampos pre_score_pos = file.tellg();
int cur_score;
while (file >> cur_score)
{
if (cur_score < new_high_score)
{
break;
}
pre_score_pos = file.tellg();
}
if (file.fail() && ! file.eof())
{
cout << "Bad score/read--exiting";
return 0;
}
file.clear();
file.seekg(pre_score_pos);
vector<int> scores;
while (file >> cur_score)
{
scores.push_back(cur_score);
}
if (! file.eof())
{
cout << "Bad score/read--exiting";
return 0;
}
file.clear();
file.seekp(pre_score_pos);
if (pre_score_pos != std::streampos(0))
{
file << endl;
}
file << new_high_score << endl;
for (vector<int>::iterator itr - scores.begin();
itr != scores.end();
++itr)
{
file << *itr << endl;
}
}
II. Практическая часть
Глава 5. Применение механизмов Visual C++ для работы с текстовыми файлами на практике
Для выполнения практического задания реализуем описанный в предыдущих главах пример работы с файлом результатов соревнований в среде Visual Studio 2017.
Запустим Visual Studio 2017 и создадим проект консольного приложения в Visual C++ (рис. 1-2).
Рисунок 1 – Среда разработки Visual Studio 2017
Для создания проекта консольного приложения выберем пункт меню «Файл/Создать/Проект» и в диалоговом окне выберем «Консольное приложение Windows» [4, с.26][16].
В графе «Имя» зададим название приложения «HightScores».
В графе «Расположение» зададим путь к проекту «d:\Projects».
В графе «Имя решения» зададим наименование решения «HightScores».
Рисунок 2 – Выбор консольного приложения
Для создания консольного приложения «HightScores» кликнем на кнопку «Ok» диалогового окна. Созданный проект отобразится в редакторе кода (рис. 3).
Прописываем заголовочные файлы и пространство имен.
Рисунок 3 – Созданный проект
Далее создаем файл «scores.txt» и записываем в него данные от 15 до 1 (рис. 4).
Листинг 1 – Код заполнения файла данными
// создаем файл и записываем в него данные от 1 до 15
ofstream file_writer("scores.txt");
if (!file_writer.is_open())
{
cout << "Could not open file!" << '\n';
return 0;
}
for (int i = 0; i < 15; i++)
{
file_writer << 15 - i << '\n';
}
Рисунок 4 – Создание и заполнение файла
Далее, читаем данные из файла «scores.txt» (рис. 5).
Листинг 2 – Код чтения данных из файла и сохранение их в векторе
// чтение данных из файла
ifstream file_reader("scores.txt");
if (!file_reader.is_open())
{
cout << "Could not open file!" << '\n';