Добавить в избранное

Работа с файлами в C++: fstream, чтение, запись и бинарный режим

Чтение ≈ 4 мин

Файловый ввод-вывод в C++: ifstream/ofstream/fstream, построчное чтение, бинарный режим, проверка ошибок, std::filesystem для путей и директорий — с готовыми рецептами для реальных задач.

Три класса для файлов

В <fstream> живут три класса, все наследуются от потоков и работают через знакомые <</>>:

  • std::ifstream — только чтение;
  • std::ofstream — только запись;
  • std::fstream — и то, и другое.
#include <fstream>
#include <iostream>
#include <string>

std::ofstream out("log.txt");        // создаст или обрежет файл
out << "первая строка\n";
out.close();                         // явно — или дождётся деструктора

std::ifstream in("log.txt");
std::string line;
while (std::getline(in, line)) {     // читаем построчно
    std::cout << line << "\n";
}

Файлы — RAII-объекты: деструктор сам закрывает дескриптор. Но проверка ошибок — ваша задача.

Проверка открытия и ошибок

std::ifstream in("data.txt");
if (!in.is_open()) {                 // или if (!in)
    std::cerr << "не удалось открыть\n";
    return 1;
}

Состояния потока: good(), eof(), fail(), bad(). Типичная правильная идиома чтения до конца:

std::string line;
while (std::getline(in, line)) {   // ok
    // обработка
}
// после цикла eof() — нормально; fail() без eof — ошибка формата

Антипаттерн while (!in.eof()) — он обрабатывает последнюю строку дважды: eof выставляется после неудачного чтения, а не до.

Запись и дозапись

std::ofstream out("log.txt");                       // перезаписать
std::ofstream app("log.txt", std::ios::app);        // дозапись в конец
std::ofstream tr("log.txt", std::ios::trunc);       // явно обрезать

out << "x = " << 42 << "\n";                        // форматированный вывод
out << std::fixed << std::setprecision(2) << 3.14159; // "3.14"

Важно: данные буферизуются. close() или деструктор сбрасывают буфер на диск. Для «напиши прямо сейчас» — out.flush() или std::endl (flush + newline).

Бинарный режим

Для не-текстовых данных открывают с std::ios::binary и работают через read/write сырыми байтами:

struct Header { uint32_t magic; uint32_t version; };

// запись структуры
std::ofstream out("data.bin", std::ios::binary);
Header h{0xDEADBEEF, 1};
out.write(reinterpret_cast<const char*>(&h), sizeof(h));

// чтение
std::ifstream in("data.bin", std::ios::binary);
Header h2{};
in.read(reinterpret_cast<char*>(&h2), sizeof(h2));

В бинарном режиме на Windows не происходит трансляция \n в \r\n — критично для строгих форматов.

Чтение всего файла сразу

// строка со всем содержимым
std::ifstream in("file.txt", std::ios::binary);
std::string content((std::istreambuf_iterator<char>(in)),
                     std::istreambuf_iterator<char>());

// или для больших файлов — через размер
in.seekg(0, std::ios::end);
auto size = in.tellg();
in.seekg(0, std::ios::beg);
std::string buf(size, '\0');
in.read(buf.data(), size);

std::filesystem: пути, папки, размер

С C++17 есть std::filesystem — кроссплатформенная работа с файловой системой:

#include <filesystem>
namespace fs = std::filesystem;

fs::exists("data.txt");
fs::file_size("data.txt");
fs::create_directories("logs/2026/10");
fs::copy_file("a.txt", "b.txt", fs::copy_options::overwrite_existing);
fs::remove("old.txt");

// обход директории
for (const auto& entry : fs::directory_iterator(".")) {
    std::cout << entry.path() << " "
              << (entry.is_directory() ? "[dir]" : "") << "\n";
}

// склейка путей — переносимо
fs::path p = fs::path("dir") / "subdir" / "file.txt";
p.extension();     // ".txt"
p.stem();          // "file"
p.filename();      // "file.txt"

Позиционирование и строки вместо файлов

in.seekg(0);                      // в начало
in.seekg(-10, std::ios::end);     // за 10 байт до конца
auto pos = in.tellg();            // текущая позиция

// std::stringstream — поток в памяти
std::stringstream ss;
ss << "x=" << 42;
std::string s = ss.str();
int x; ss >> x;                   // парсинг из строки

Типичные ошибки

  • while (!eof()) — лишняя итерация; пишите while (getline(...)).
  • Не проверять открытие — молчаливые пустые данные.
  • << в бинарный файл — смешение текстового и бинарного вывода.
  • Чтение read без проверки gcount() — частичное чтение не заметите.
  • Разделители путей / vs \ — используйте std::filesystem::path или / (работает и на Windows).

Итог

Минимум: ifstream/ofstream для текста, ios::binary + read/write для байтов, getline в цикле, проверка is_open, std::filesystem для путей и директорий. Файлы — RAII, деструктор сам всё закроет; за вами остаётся только проверка ошибок и правильный режим.

Полный пример: логгер в файл

class Logger {
    std::ofstream out_;
public:
    Logger(const std::string& path) : out_(path, std::ios::app) {
        if (!out_) throw std::runtime_error("не открыть лог: " + path);
    }
    void log(std::string_view msg) {
        out_ << timestamp() << " " << msg << '\n';
        // flush дорого — не делаем на каждую строку; destructor сделает при закрытии
    }
    ~Logger() { out_ << "--- конец сессии ---\n"; }  // автозакрытие
};

Работа с путями: std::filesystem подробнее

namespace fs = std::filesystem;

fs::path p = "/var/log/app/error.log";
p.filename();        // "error.log"
p.stem();            // "error"
p.extension();       // ".log"
p.parent_path();     // "/var/log/app"

fs::create_directories("/tmp/a/b/c");       // mkdir -p
for (const auto& e : fs::directory_iterator("/tmp"))
    if (e.is_regular_file()) std::cout << e.path() << '\n';

fs::remove("temp.txt");
fs::rename("old.txt", "new.txt");
fs::copy_file("a", "b", fs::copy_options::overwrite_existing);

Парсинг простого конфига

std::unordered_map<std::string, std::string> readConfig(const std::string& path) {
    std::unordered_map<std::string, std::string> cfg;
    std::ifstream f(path);
    std::string line;
    while (std::getline(f, line)) {
        auto pos = line.find('=');
        if (pos == std::string::npos) continue;
        cfg[line.substr(0, pos)] = line.substr(pos + 1);
    }
    return cfg;
}

Бинарная запись структуры — осторожно

Записывать struct напрямую через write(reinterpret_cast<char*>(&s), sizeof(s)) рискованно: padding-байты и порядок байт делают файл непереносимым между платформами и компиляторами. Для сериализации — явные форматы (JSON, protobuf, собственная упаковка полей).

Обработка ошибок стримов

Стримы имеют флаги: good(), fail(), bad(), eof(). eof становится true только после попытки чтения за концом — классическая ошибка while(!eof()) читает лишнюю итерацию. Правильно: while (getline(f, line)) или while (f >> x) — проверка самого чтения.

Поделиться
Понравился материал?

Комментарии

Добавить комментарий