Работа файлового ввода/вывода в C++ почти аналогична работе обычных потоков ввода/вывода (но с небольшими нюансами). Есть три основных класса файлового ввода/вывода в C++:
ifstream (является дочерним классу istream);
ofstream (является дочерним классу ostream);
fstream (является дочерним классу iostream).
С помощью этих классов можно выполнять однонаправленный файловый ввод, однонаправленный файловый вывод и двунаправленный файловый ввод/вывод. Для их использования нужно всего лишь подключить заголовочный файл fstream.
В отличие от потоков cout, cin, cerr и clog, которые сразу же можно использовать, файловые потоки должны быть явно установлены программистом. То есть, чтобы открыть файл для чтения и/или записи, нужно создать объект соответствующего класса файлового ввода/вывода, указав имя файла в качестве параметра. Затем, с помощью операторов вставки (<<) или извлечения (>>), можно записывать данные в файл или читать содержимое файла. После этого финал — нужно закрыть файл: явно вызвать метод close() или просто позволить файловой переменной ввода/вывода выйти из области видимости (деструктор файлового класса ввода/вывода закроет этот файл автоматически вместо нас).
Файловый вывод
Для записи в файл используется класс ofstream. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <iostream> #include <fstream> #include <cstdlib> // для использования exit() int main() { using namespace std; // ofstream используется для записи данных в файл // Создаём файл SomeText.txt ofstream outf("SomeText.txt"); // Если мы не можем открыть этот файл для записи данных в него if (!outf) { // То выводим сообщение об ошибке и выполняем exit() cerr << "Uh oh, SomeText.txt could not be opened for writing!" << endl; exit(1); } // Записываем в файл следующие две строчки outf << "See line #1!" << endl; outf << "See line #2!" << endl; return 0; // Когда outf выйдет из области видимости, деструктор класса ofstream автоматически закроет наш файл } |
Если вы загляните в каталог вашего проекта (ПКМ по вкладке с названием вашего .cpp файла в Visual Studio > «Открыть содержащую папку»), то увидите файл с именем SomeText.txt, в котором находятся следующие строчки:
See line #1!
See line #2!
Обратите внимание, мы также можем использовать метод put() для записи одного символа в файл.
Файловый ввод
Теперь мы попытаемся прочитать содержимое файла, который создали в предыдущем примере. Обратите внимание, ifstream возвращает 0, если мы достигли конца файла (это удобно для определения «длины» содержимого файла). Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <iostream> #include <fstream> #include <string> #include <cstdlib> // для использования exit() int main() { using namespace std; // ifstream используется для чтения содержимого файла // Попытаемся прочитать содержимое файла SomeText.txt ifstream inf("SomeText.txt"); // Если мы не можем открыть этот файл для чтения его содержимого if (!inf) { // То выводим следующее сообщение об ошибке и выполняем exit() cerr << "Uh oh, SomeText.txt could not be opened for reading!" << endl; exit(1); } // Пока есть данные, которые мы можем прочитать while (inf) { // То перемещаем эти данные в строку, которую затем выводим на экран string strInput; inf >> strInput; cout << strInput << endl; } return 0; // Когда inf выйдет из области видимости, деструктор класса ifstream автоматически закроет наш файл } |
Результат выполнения программы выше:
See
line
#1!
See
line
#2!
Хм, это не совсем то, что мы хотели. Как мы уже знаем из предыдущих уроков, оператор извлечения работает с «отформатированными данными», т.е. он игнорирует все пробелы, символы табуляции и новой строки. Чтобы прочитать всё содержимое как есть, без его разбивки на части (как в примере выше), нам нужно использовать метод getline():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <iostream> #include <fstream> #include <string> #include <cstdlib> // для использования exit() int main() { using namespace std; // ifstream используется для чтения содержимого файлов // Мы попытаемся прочитать содержимое файла SomeText.txt ifstream inf("SomeText.txt"); // Если мы не можем открыть файл для чтения его содержимого if (!inf) { // То выводим следующее сообщение об ошибке и выполняем exit() cerr << "Uh oh, SomeText.txt could not be opened for reading!" << endl; exit(1); } // Пока есть, что читать while (inf) { // То перемещаем то, что можем прочитать, в строку, а затем выводим эту строку на экран string strInput; getline(inf, strInput); cout << strInput << endl; } return 0; // Когда inf выйдет из области видимости, деструктор класса ifstream автоматически закроет наш файл } |
Результат выполнения программы выше:
Буферизованный вывод
Вывод в C++ может быть буферизован. Это означает, что всё, что выводится в файловый поток, не может сразу же быть записанным на диск (в конкретный файл). Это сделано, в первую очередь, по соображениям производительности. Когда данные буфера записываются на диск, то это называется очисткой буфера. Один из способов очистки буфера является закрытие файла. В таком случае всё содержимое буфера будет перемещено на диск, а затем файл будет закрыт.
Буферизация вывода обычно не является проблемой, но при определённых обстоятельствах она может вызвать проблемы в неосторожных новичков. Например, когда в буфере хранятся данные и программа преждевременно завершает своё выполнение (либо в результате сбоя, либо путём вызова метода exit()). В таких случаях деструкторы классов файлового ввода/вывода не выполняются, файлы никогда не закрываются, буферы не очищаются и наши данные теряются навсегда. Вот почему хорошей идеей является явное закрытие всех открытых файлов перед вызовом exit().
Также буфер можно очистить вручную, используя метод ostream::flush() или отправив std::flush в выходной поток. Любой из этих способов может быть полезен для обеспечения немедленной записи содержимого буфера на диск в случае сбоя программы.
Интересный нюанс: Поскольку std::endl;
также очищает выходной поток, то его чрезмерное использование (приводящее к ненужным очисткам буфера) может повлиять на производительность программы (так как очистка буфера в некоторых случаях может быть затратной операцией). По этой причине программисты, которые заботятся о производительности своего кода, часто используют \n
вместо std::endl
для вставки символа новой строки в выходной поток, дабы избежать ненужной очистки буфера.
Режимы открытия файлов
Что произойдёт, если мы попытаемся записать данные в уже существующий файл? Повторный запуск программы выше (самая первая) показывает, что исходный файл полностью перезаписывается при повторном запуске программы. А что если нам нужно добавить данные в конец файла? Оказывается, конструкторы файлового потока принимают необязательный второй параметр, который позволяет указать программисту способ открытия файла. В качестве этого параметра можно передавать следующие флаги (которые находятся в классе ios):
app – открывает файл в режиме добавления;
ate – переходит в конец файла перед чтением/записью;
binary – открывает файл в бинарном режиме (вместо текстового режима);
in – открывает файл в режиме чтения (по умолчанию для ifstream);
out – открывает файл в режиме записи (по умолчанию для ofstream);
trunc – удаляет файл, если он уже существует.
Можно указать сразу несколько флагов путём использования побитового ИЛИ (|).
ifstream по умолчанию работает в режиме ios::in;
ofstream по умолчанию работает в режиме ios::out;
fstream по умолчанию работает в режиме ios::in ИЛИ ios::out, что означает, что вы можете выполнять как чтение содержимого файла, так и запись данных в файл.
Теперь давайте напишем программу, которая добавит две строки в ранее созданный файл SomeText.txt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <iostream> #include <cstdlib> // для использования exit() #include <fstream> int main() { using namespace std; // Передаём флаг ios:app, чтобы сообщить fstream, что мы собираемся добавить свои данные к уже существующим данным файла, // мы не собираемся перезаписывать файл. Нам не нужно передавать флаг ios::out, // поскольку ofstream по умолчанию работает в режиме ios::out ofstream outf("SomeText.txt", ios::app); // Если мы не можем открыть файл для записи данных if (!outf) { // То выводим следующее сообщение об ошибке и выполняем exit() cerr << "Uh oh, SomeText.txt could not be opened for writing!" << endl; exit(1); } outf << "See line #3!" << endl; outf << "See line #4!" << endl; return 0; // Когда outf выйдет из области видимости, деструктор класса ofstream автоматически закроет наш файл } |
Теперь, если мы посмотрим на SomeText.txt (запустим одну из программ выше с чтением файла или откроем этот файл в каталоге проекта), то увидим следующее:
See line #1!
See line #2!
See line #3!
See line #4!
Явное открытие файлов с помощью метода open()
Точно так же, как мы явно закрываем файл с помощью метода close(), точно так же мы можем и явно открывать файл с помощью метода open(). open() работает аналогично конструкторам класса файлового ввода/вывода: принимает имя файла и режим (необязательно), в котором нужно открыть файл, в качестве параметров. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <fstream> int main() { using namespace std; ofstream outf("SomeText.txt"); outf << "See line #1!" << endl; outf << "See line #2!" << endl; outf.close(); // явно закрываем файл // Упс, мы кое-что забыли сделать outf.open("SomeText.txt", ios::app); outf << "See line #3!" << endl; outf.close(); return 0; // Когда outf выйдет из области видимости, деструктор класса ofstream автоматически закроет наш файл } |
Результат:
See line #1!
See line #2!
See line #3!
На этом всё. В следующем уроке мы рассмотрим рандомный файловый ввод/вывод.