БлогNot. Лекции по C/C++: работа с файлами (fstream)
Лекции по C/C++: работа с файлами (fstream)
Механизм ввода-вывода, разработанный для обычного языка С, не соответствует общепринятому сегодня стилю объектно-ориентированного программирования, кроме того, он активно использует операции с указателями, считающиеся потенциально небезопасными в современных защищённых средах выполнения кода. Альтернативой при разработке прикладных приложений является механизм стандартных классов ввода-вывода, предоставляемый стандартом языка C++.
Наиболее часто применяются классы ifstream для чтения, ofstream для записи и fstream для модификации файлов.
Все поточные классы ввода-вывода являются косвенными производными от общего предка ios , полностью наследуя его функциональность. Так, режим открытия файлов задает член данных перечисляемого типа open_mode, который определяется следующим образом:
Ниже приведены возможные значения флагов и их назначение.
| Режим | Назначение |
| in | Открыть для ввода (выбирается по умолчанию для ifstream) |
| out | Открыть для вывода (выбирается по умолчанию для ofstream) |
| binary | Открыть файл в бинарном виде |
| aрр | Присоединять данные; запись в конец файла |
| ate | Установить файловый указатель на конец файла |
| trunc | Уничтожить содержимое, если файл существует (выбирается по умолчанию, если флаг out указан, а флаги ate и арр — нет) |
Например, чтобы открыть файл с именем test.txt для чтения данных в бинарном виде, следует написать:
Оператор логического ИЛИ ( | ) позволяет составить режим с любым сочетанием флагов. Так, чтобы, открывая файл по записи, случайно не затереть существующий файл с тем же именем, надо использовать следующую форму:
Предполагается, что к проекту подключён соответствующий заголовочный файл:
Для проверки того удалось ли открыть файл, можно применять конструкцию
Операторы включения и извлечения
Переопределённый в классах работы с файлами оператор включения ( ) записывает данные в файловый поток. Как только вы открыли файл для записи, можно записывать в него текстовую строку целиком:
Можно также записывать текстовую строку по частям:
Оператор endl завершает ввод строки символом "возврат каретки":
С помощью оператора включения несложно записывать в файл значения переменных или элементов массива:
В результате выполнения кода образуется три строки текстового файла Temp.txt :
Обратите внимание, что числовые значения записываются в файл в виде текстовых строк, а не двоичных значений.
Оператор извлечения ( >> )производит обратные действия. Казалось бы, чтобы извлечь символы из файла Temp.txt , записанного ранее, нужно написать код наподобие следующего:
Однако оператор извлечения остановится на первом попавшемся разделителе (символе пробела, табуляции или новой строки). Таким образом, при разборе предложения "Текстовый массив содержит переменные" только слово "Текстовый" запишется в массив buff , пробел игнорируется, а слово "массив" станет значением целой переменной vx и исполнение кода "пойдет вразнос" с неминуемым нарушением структуры данных. Далее, при обсуждении класса ifstream , будет показано, как правильно организовать чтение файла из предыдущего примера.
Класс ifstream: чтение файлов
Как следует из расшифровки названия, класс ifstream предназначен для ввода файлового потока. Далее перечислены основные методы класса. Большая часть из них унаследована от класса istream и перегружена с расширением родительской функциональности. К примеру, функция get , в зависимости от параметра вызова, способна считывать не только одиночный символ, но и символьный блок.
| Метод | Описание |
| open | Открывает файл для чтения |
| get | Читает один или более символов из файла |
| getline | Читает символьную строку из текстового файла или данные из бинарного файла до определенного ограничителя |
| read | Считывает заданное число байт из файла в память |
| eof | Возвращает ненулевое значение (true), когда указатель потока достигает конца файла |
| peek | Выдает очередной символ потока, но не выбирает его |
| seekg | Перемещает указатель позиционирования файла в заданное положение |
| tellg | Возвращает текущее значение указателя позиционирования файла |
| close | Закрывает файл |
Теперь понятно, как нужно модифицировать предыдущий пример, чтобы использование оператора извлечения данных давало ожидаемый результат:
Метод getline прочитает первую строку файла до конца, а оператор >> присвоит значения переменным.
Следующий пример показывает добавление данных в текстовый файл с последующим чтением всего файла. Цикл while (1) используется вместо while(!file2.eof()) по причинам, которые обсуждались в предыдущей лекции.
В следующем примере показан цикл считывания строк из файла test.txt и их отображения на консоли.
Этот код под ОС Windows также зависит от наличия в последней строке файла символа перевода строки, надежнее было бы сделать так:
Явные вызовы методов open и close не обязательны. Действительно, вызов конструктора с аргументом позволяет сразу же, в момент создания поточного объекта file , открыть файл:
Вместо метода close можно использовать оператор delete , который автоматически вызовет деструктор объекта file и закроет файл. Код цикла while обеспечивает надлежащую проверку признака конца файла.
Класс ofstream: запись файлов
Класс ofstream предназначен для вывода данных из файлового потока. Далее перечислены основные методы данного класса.
| Метод | Описание |
| open | Открывает файл для записи |
| put | Записывает одиночный символ в файл |
| write | Записывает заданное число байт из памяти в файл |
| seekp | Перемещает указатель позиционирования в указанное положение |
| tellp | Возвращает текущее значение указателя позиционирования файла |
| close | Закрывает файл |
Описанный ранее оператор включения удобен для организации записи в текстовый файл:
В принципе, бинарные данные обслуживаются наподобие текстовых. Отличие состоит в том, что если бинарные данные записываются в определенной логической структуре, то они должны считываться из файла в переменную того же структурного типа.
Первый параметр методов write и read (адрес блока записи/чтения) должен иметь тип символьного указателя char * , поэтому необходимо произвести явное преобразование типа адреса структуры void * . Второй параметр указывает, что бинарные блоки файла имеют постоянный размер байтов независимо от фактической длины записи. Следующее приложение дает пример создания и отображения данных простейшей записной книжки. Затем записи файла последовательно считываются и отображаются на консоли.
В результате выполнения этого кода образуется бинарный файл Notebook.dat из трех блоков размером по 80 байт каждый (при условии, что символы — однобайтовые). Естественно, вы можете использовать другие поточные методы и проделывать любые операции над полями определенной структуры данных.
Класс fstream: произвольный доступ к файлу
Предположим что в нашей записной книжке накопилось 100 записей, а мы хотим считать 50-ю. Конечно, можно организовать цикл и прочитать все записи с первой по заданную. Очевидно, что более целенаправленное решение — установить указатель позиционирования файла pos прямо на запись 50 и считать ее:
Подобные операции поиска эффективны, если файл состоит из записей известного и постоянного размера. Чтобы заменить содержимое произвольной записи, надо открыть поток вывода в режиме модификации:
Если не указать флаг ios::ate (или ios::app ), то при открытии бинарного файла Notebook.dat его предыдущее содержимое будет стерто!
Наконец, можно открыть файл одновременно для чтения/записи, используя методы, унаследованные поточным классом fstream от своих предшественников. Поскольку класс fstream произведен от istream и ostream (родителей ifstream и ofstream соответственно), все упомянутые ранее методы становятся доступными в приложении.
В следующем примере показана перестановка первой и третьей записей файла Notebook.dat .
В конструкторе объекта file надо указать флаги ios::in и ios::out , разрешая одновременное выполнение операций чтения и записи. В результате выполнения этого кода первая и третья записи бинарного файла Notebook.dat поменяются местами.
Дополнительные примеры по теме есть в этой заметке.
Оглавление серии
05.11.2015, 09:45; рейтинг: 103433
Обновл. 15 Июл 2019 |
Работа файлового ввода/вывода в C++ почти аналогична работе обычных потоков ввода/вывода (но с небольшими нюансами).
Классы файлового ввода/вывода
Есть три основных класса файлового ввода/вывода в C++:
ofstream (является дочерним классу ostream);
fstream (является дочерним классу iostream ).
С помощью этих классов можно выполнять однонаправленный файловый ввод, однонаправленный файловый вывод и двунаправленный файловый ввод/вывод. Для их использования нужно всего лишь подключить заголовочный файл fstream.
В отличие от потоков cout, cin, cerr и clog, которые сразу же можно использовать, файловые потоки должны быть явно установлены программистом. То есть, чтобы открыть файл для чтения и/или записи, нужно создать объект соответствующего класса файлового ввода/вывода, указав имя файла в качестве параметра. Затем, с помощью операторов вставки ( >), можно записывать данные в файл или читать содержимое файла. После этого финал — нужно закрыть файл: явно вызвать метод close() или просто позволить файловой переменной ввода/вывода выйти из области видимости (деструктор файлового класса ввода/вывода закроет этот файл автоматически вместо нас).
Файловый вывод
Для записи в файл используется класс ofstream . Например:
Если вы загляните в каталог вашего проекта (ПКМ по вкладке с названием вашего .cpp файла в Visual Studio > «Открыть содержащую папку»), то увидите файл с именем SomeText.txt, в котором находятся следующие строчки:
See line #1!
See line #2!
Обратите внимание, мы также можем использовать метод put() для записи одного символа в файл.
Файловый ввод
Теперь мы попытаемся прочитать содержимое файла, который создали в предыдущем примере. Обратите внимание, ifstream возвращает 0 , если мы достигли конца файла (это удобно для определения «длины» содержимого файла). Например:
Результат выполнения программы выше:
Хм, это не совсем то, что мы хотели. Как мы уже знаем из предыдущих уроков, оператор извлечения работает с «отформатированными данными», т.е. он игнорирует все пробелы, символы табуляции и символ новой строки. Чтобы прочитать всё содержимое как есть, без его разбивки на части (как в примере выше), нам нужно использовать метод getline():
Результат выполнения программы выше:
See line #1!
See line #2!
Буферизованный вывод
Вывод в C++ может быть буферизован. Это означает, что всё, что выводится в файловый поток, не может сразу же быть записанным на диск (в конкретный файл). Это сделано, в первую очередь, по соображениям производительности. Когда данные буфера записываются на диск, то это называется очисткой буфера. Одним из способов очистки буфера является закрытие файла. В таком случае всё содержимое буфера будет перемещено на диск, а затем файл будет закрыт.
Буферизация вывода обычно не является проблемой, но, при определённых обстоятельствах, она может вызвать проблемы у неосторожных новичков. Например, когда в буфере хранятся данные и программа преждевременно завершает своё выполнение (либо в результате сбоя, либо путём вызова метода exit()). В таких случаях деструкторы классов файлового ввода/вывода не выполняются, файлы никогда не закрываются, буферы не очищаются и наши данные теряются навсегда. Вот почему хорошей идеей является явное закрытие всех открытых файлов перед вызовом exit().
Также буфер можно очистить вручную, используя метод ostream::flush() или отправив std::flush в выходной поток. Любой из этих способов может быть полезен для обеспечения немедленной записи содержимого буфера на диск в случае сбоя программы.
Интересный нюанс: Поскольку std::endl; также очищает выходной поток, то его чрезмерное использование (приводящее к ненужным очисткам буфера) может повлиять на производительность программы (так как очистка буфера в некоторых случаях может быть затратной операцией). По этой причине программисты, которые заботятся о производительности своего кода, часто используют
вместо std::endl для вставки символа новой строки в выходной поток, дабы избежать ненужной очистки буфера.
Режимы открытия файлов
Что произойдёт, если мы попытаемся записать данные в уже существующий файл? Повторный запуск программы выше (самая первая) показывает, что исходный файл полностью перезаписывается при повторном запуске программы. А что, если нам нужно добавить данные в конец файла? Оказывается, конструкторы файлового потока принимают необязательный второй параметр, который позволяет указать программисту способ открытия файла. В качестве этого параметра можно передавать следующие флаги (которые находятся в классе ios ):
app — открывает файл в режиме добавления;
ate — переходит в конец файла перед чтением/записью;
binary — открывает файл в бинарном режиме (вместо текстового режима);
in — открывает файл в режиме чтения (по умолчанию для ifstream );
out — открывает файл в режиме записи (по умолчанию для ofstream );
trunc — удаляет файл, если он уже существует.
Можно указать сразу несколько флагов путём использования побитового ИЛИ (|).
ifstream по умолчанию работает в режиме ios::in;
ofstream по умолчанию работает в режиме ios::out;
fstream по умолчанию работает в режиме ios::in ИЛИ ios::out, что означает, что вы можете выполнять как чтение содержимого файла, так и запись данных в файл.
Работа с файлами с использованием конструкций языка Си была рассмотрена здесь.
Для программиста открытый файл представляется как последовательность считываемых или записываемых данных. При открытии файла с ним связывается поток ввода-вывода . Выводимая информация записывается в поток, вводимая информация считывается из потока.
Для работы с файлами необходимо подключить заголовочный файл . В нем определены несколько классов и подключены заголовочные файлы
Файловый ввод-вывод аналогичен стандартному вводу-выводу, единственное отличие – это то, что ввод-вывод выполнятся не на экран, а в файл.
Если ввод-вывод на стандартные устройства выполняется с помощью объектов cin и cout , то для организации файлового ввода-вывода достаточно создать собственные объекты, которые можно использовать аналогично этим операторам.
При работе с файлом можно выделить следующие этапы:
- создать объект класса fstream (возможно, ofstream или ifstream );
- связать объект класса fstream с файлом, который будет использоваться для операций ввода-вывода;
- осуществить операции ввода-вывода в файл;
- закрыть файл.
В результате будет создан файл 
Режимы открытия файлов устанавливают характер использования файлов. Для установки режима в классе ios предусмотрены константы, которые определяют режим открытия файлов.
| Константа | Описание |
| ios::in | открыть файл для чтения |
| ios::out | открыть файл для записи |
| ios::ate | при открытии переместить указатель в конец файла |
| ios::app | открыть файл для записи в конец файла |
| ios::trunc | удалить содержимое файла, если он существует |
| ios::binary | открытие файла в двоичном режиме |
Режимы открытия файлов можно устанавливать непосредственно при создании объекта или при вызове метода open() .
Режимы открытия файлов можно комбинировать с помощью поразрядной логической операции ИЛИ | , например:
ios::out | ios::in — открытие файла для записи и чтения.
Произвольный доступ к файлу
Система ввода-вывода С++ позволяет осуществлять произвольный доступ с использованием методов seekg() и seekp() .
Смещение определяет область значений в пределах файла ( long int ).
Система ввода-вывода С++ обрабатывает два указателя, ассоциированные с каждым файлом:
- get pointer g — определяет, где именно в файле будет производиться следующая операция ввода;
- put pointer p — определяет, где именно в файле будет производиться следующая операция вывода.
Позиция смещения определяется как
| Позиция | Значение |
| ios::beg | Начало файла |
| ios::cur | Текущее положение |
| ios::end | Конец файла |
Всякий раз, когда осуществляются операции ввода или вывода, соответствующий указатель автоматически перемещается.
С помощью методов seekg() и seekp() можно получить доступ к файлу в произвольном месте.
Можно определить текущую позицию файлового указателя, используя следующие функции:
- streampos tellg() — позиция для ввода
- streampos tellp() — позиция для вывода
В результате выполнения первой части программы будет создан файл
Вторая часть программы выведет в консоль 
Ещё один пример. Допустим, нам нужно заполнять таблицу
| ФИО | Дата рождения | Хобби |
Причем каждая вновь введенная строка должна размещаться в таблице непосредственно под "шапкой".
Алгоритм решения задачи следующий:
- формируем очередную строку для вывода
- открываем файл для чтения, считываем из него данные и сохраняем их в массив строк
- закрываем файл
- открываем файл для записи
- выводим "шапку" таблицы
- выводим новую строку
- выводим все сохраненные строки обратно в файл, начиная со строки после шапки
#include
#include
using namespace std;
#define LINES 100 // максимальное количество строк в файле
int main() <
system( "chcp 1251" );
system( "cls" );
char line[LINES][100];
char str[30];
char s[] = "| | | |" ;
// Ввод данных для размещаемой строки
cout "ФИО: " ;
cin.getline(str, 30); // вводим ФИО
for ( int i = 0; str[i] != ‘
