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();
