Работа файлового ввода/вывода в языке 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!
На этом всё! На следующем уроке мы рассмотрим рандомный файловый ввод/вывод.