23.6 – Основы файлового ввода/вывода
Файловый ввод/вывод в C++ работает очень похоже на обычный ввод/вывод (с небольшими дополнительными сложностями). В C++ есть 3 основных класса файлового ввода/вывода: ifstream
(производный от istream
), ofstream
(производный от ostream
) и fstream
(производный от iostream
). Эти классы выполняют файловый ввод, вывод и ввод/вывод соответственно. Чтобы использовать классы файлового ввода/вывода, вам необходимо включить заголовок fstream
.
В отличие от потоков cout
, cin
, cerr
и clog
, которые уже готовы к использованию, файловые потоки должны быть настроены программистом явно. Однако это очень просто: чтобы открыть файл для чтения и/или записи, просто создайте экземпляр объекта соответствующего класса файлового ввода/вывода с именем файла в качестве параметра. Затем используйте операторы вставки и извлечения для записи или чтения данных из файла. Как только вы закончите, есть несколько способов закрыть файл: явно вызвать функцию close()
или просто позволить переменной ввода/вывода файла выйти за пределы области видимости (деструктор класса файлового ввода/вывода закроет файл за вас).
Вывод в файл
Для вывода в файл в следующем примере мы собираемся использовать класс ofstream
. Это очень просто:
#include <fstream>
#include <iostream>
int main()
{
// ofstream используется для записи в файлы
// Создадим файл с именем Sample.dat
std::ofstream outf{ "Sample.dat" };
// Если мы не смогли открыть выходной файловый поток для записи
if (!outf)
{
// Распечатываем ошибку и выходим
std::cerr << "Uh oh, Sample.dat could not be opened for writing!" << std::endl;
return 1;
}
// Запишем в этот файл две строки
outf << "This is line 1" << '\n';
outf << "This is line 2" << '\n';
return 0;
// Когда outf выходит из области видимости,
// деструктор ofstream закроет файл
}
Если вы заглянете в каталог своего проекта, вы должны увидеть файл с именем Sample.dat. Если вы откроете его в текстовом редакторе, то увидите, что он действительно содержит две строки, которые мы записали в файл.
Обратите внимание, что для записи одного символа в файл можно использовать функцию put()
.
Ввод из файла
Теперь возьмем файл, который мы написали в предыдущем примере, и прочитаем его с диска. Обратите внимание, что ifstream
возвращает 0, если мы достигли конца файла (EOF, «end of the flile»). Мы воспользуемся этим фактом, чтобы определить, как долго нужно читать.
#include <fstream>
#include <iostream>
#include <string>
int main()
{
// ifstream используется для чтения файлов
// Мы будем читать из файла с именем Sample.dat
std::ifstream inf{ "Sample.dat" };
// Если мы не смогли открыть входной файловый поток для чтения
if (!inf)
{
// Распечатываем ошибку и выходим
std::cerr << "Uh oh, Sample.dat could not be opened for reading!" << std::endl;
return 1;
}
// Пока еще есть, что прочитать
while (inf)
{
// считываем информацию из файла в строку и распечатываем ее
std::string strInput;
inf >> strInput;
std::cout << strInput << '\n';
}
return 0;
// Когда inf выходит за пределы области видимости,
// деструктор ifstream закроет файл
}
Эта программа дает следующий результат:
This
is
line
1
This
is
line
2
Хм, это было не совсем то, что мы хотели. Помните, что оператор извлечения разбивает входные строки пробелами. Чтобы читать строки полностью, нам нужно использовать функцию getline()
.
#include <fstream>
#include <iostream>
#include <string>
int main()
{
// ifstream используется для чтения файлов
// Мы будем читать из файла с именем Sample.dat
std::ifstream inf{ "Sample.dat" };
// Если мы не смогли открыть входной файловый поток для чтения
if (!inf)
{
// Распечатываем ошибку и выходим
std::cerr << "Uh oh, Sample.dat could not be opened for reading!\n";
return 1;
}
// Пока еще есть, что прочитать
while (inf)
{
// считываем информацию из файла в строку и распечатываем ее
std::string strInput;
std::getline(inf, strInput);
std::cout << strInput << '\n';
}
return 0;
// Когда inf выходит за пределы области видимости,
// деструктор ifstream закроет файл
}
Эта программа дает следующий результат:
This is line 1
This is line 2
Буферизованный вывод
Вывод в C++ может буферизоваться. Это означает, что всё, что выводится в файловый поток, может сразу не записываться на диск. Вместо этого несколько операций вывода могут быть объединены и обработаны вместе. Это сделано в первую очередь из соображений производительности. Когда буфер записывается на диск, это называется очисткой/сбросом буфера (англоязычный термин «flushing»). Один из способов вызвать сброс буфера – закрыть файл: содержимое буфера будет сброшено на диск, а затем файл будет закрыт.
Буферизация обычно не проблема, но в определенных обстоятельствах при неосторожности она может вызвать сложности. Главная проблема в этом случае – когда в буфере есть данные, а программа немедленно завершается (либо из-за сбоя, либо из-за вызова exit()
). В этих случаях деструкторы классов файловых потоков не выполняются, что означает, что файлы не закрываются, что означает, что буферы не сбрасываются. В этом случае данные в буфере не записываются на диск и теряются навсегда. Вот почему всегда рекомендуется явно закрывать все открытые файлы перед вызовом exit()
.
Можно очистить буфер вручную, используя функцию ostream::flush()
или отправив std::flush
в выходной поток. Любой из этих методов может быть полезен для обеспечения немедленной записи содержимого буфера на диск на случай сбоя программы.
Одно интересное замечание: std::endl
; также очищает выходной поток. Следовательно, чрезмерное использование std::endl
(вызывающее ненужную очистку буфера) может повлиять на производительность при выполнении буферизованного ввода/вывода, когда операции очистки дороги (например, запись в файл). По этой причине программисты, заботящиеся о производительности, для вставки новой строки в выходной поток часто используют '\n' вместо std::endl
, чтобы избежать ненужной очистки буфера.
Режимы открытия файлов
Что произойдет, если мы попытаемся записать данные в уже существующий файл? Повторный запуск примера вывода в файл показывает, что при каждом запуске программы исходный файл полностью перезаписывается. Что, если вместо этого мы захотим добавить еще немного данных в конец файла? Оказывается, конструкторы файловых потоков принимают необязательный второй параметр, который позволяет указать, как следует открывать файл. Этот параметр называется режимом, и допустимые флаги, которые он принимает, находятся в классе ios
.
Режим открытия файла ios | Значение |
---|---|
app | Открывает файл в режиме добавления |
ate | Ищет конец файла перед чтением/записью |
binary | Открывает файл в двоичном режиме (вместо текстового режима) |
in | Открывает файл в режиме чтения (по умолчанию для ifstream ) |
out | Открывает файл в режиме записи (по умолчанию для ofstream ) |
trunc | Стирает файл, если он уже существует |
Можно указать несколько флагов путем их объединения с помощью побитового ИЛИ (с помощью оператора |
). ifstream
по умолчанию использует режим std::ios::in
. ofstream
по умолчанию использует режим std::ios::out
. fstream
по умолчанию имеет режим std::ios::in | std::ios::out
, то есть по умолчанию вы можете и читать, и писать.
Совет
Из-за того, как fstream
был разработан, он может дать сбой, если используется std::ios::in
, и открываемый файл не существует. Если вам нужно создать новый файл с помощью fstream
, используйте режим только std::ios::out
.
Давайте напишем программу, которая добавляет еще две строки к ранее созданному файлу Sample.dat:
#include <iostream>
#include <fstream>
int main()
{
// Мы передадим флаг ios::app, чтобы сообщить ofstream о необходимости добавления
// вместо того, чтобы перезаписывать файл. Нам не нужно передавать std::ios::out,
// поскольку std::ios::out для ofstream используется по умолчанию
std::ofstream outf{ "Sample.dat", std::ios::app };
// Если мы не смогли открыть выходной файловый поток для записи
if (!outf)
{
// Распечатываем ошибку и выходим
std::cerr << "Uh oh, Sample.dat could not be opened for writing!\n";
return 1;
}
outf << "This is line 3" << '\n';
outf << "This is line 4" << '\n';
return 0;
// Когда outf выходит из области видимости,
// деструктор ofstream закроет файл
}
Теперь, если мы взглянем на Sample.dat (используя одну из приведенных выше программ-примеров, которая печатает его содержимое, или используя текстовый редактор), мы увидим следующее:
This is line 1
This is line 2
This is line 3
This is line 4
Явное открытие файлов с помощью open()
Также файловый поток можно явно открыть с помощью open()
и явно закрыть его с помощью close()
. open()
работает так же, как конструкторы файловых потоков – она принимает имя файла и необязательный параметр режима открытия файла.
Например:
std::ofstream outf{ "Sample.dat" };
outf << "This is line 1" << '\n';
outf << "This is line 2" << '\n';
outf.close(); // явно закрываем файл
// Ой, мы что-то забыли
outf.open("Sample.dat", std::ios::app);
outf << "This is line 3\n";
outf.close();