23.6 – Основы файлового ввода/вывода

Добавлено 12 октября 2021 в 13:19

Файловый ввод/вывод в 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();

Теги

C++ / CppiostreamLearnCppstd::fstreamstd::ifstreamstd::ofstreamВвод/выводДля начинающихОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.