Файлы и потоки в C++: чек-лист перед продакшеном
Открыли файл, записали, забыли проверить ошибку — и в проде получили молча потерянные данные. Разбираем, что проверить в работе с fstream перед релизом.

Типичная картина: сервис пишет отчёты в файл, тесты зелёные, а через месяц оказывается, что часть записей потерялась, потому что диск был заполнен, а код этого не заметил. std::ofstream по умолчанию не бросает исключений — он молча выставляет флаг состояния, который никто не проверяет. Это чек-лист по типичным местам, где работа с файлами и потоками в C++ ломается на боевой нагрузке. Актуально для C++17 и C++20 (GCC 12+, Clang 15+, MSVC 2022).
Формат — практика: не пересказ справочника по <fstream>, а конкретные ошибки и то, чем за них платят. Кодировки, парсинг форматов и сетевые потоки — отдельные темы, здесь только файлы и стандартные потоки.
1. Проверка ошибок: флаги против исключений
Главная ловушка: поток в состоянии ошибки продолжает «работать» — методы вызываются, но ничего не делают. Классический баг:
#include <fstream>
#include <string>
void save_report(const std::string& path, const std::string& data) {
std::ofstream out(path);
out << data; // если файл не открылся — данные ушли в никуда
} // деструктор закрыл поток, ошибку никто не увиделЕсли каталога нет или нет прав на запись, out просто в состоянии fail. Программа завершится успешно, файла не будет. Минимум — проверять поток явно:
#include <fstream>
#include <stdexcept>
#include <string>
void save_report(const std::string& path, const std::string& data) {
std::ofstream out(path);
if (!out) {
throw std::runtime_error("не удалось открыть файл: " + path);
}
out << data;
out.flush(); // принудительно вытолкнуть буфер
if (!out) {
throw std::runtime_error("ошибка записи: " + path);
}
}Альтернатива — включить исключения через exceptions(). Тогда поток сам бросит std::ios_base::failure при ошибке:
std::ofstream out;
out.exceptions(std::ofstream::failbit | std::ofstream::badbit);
out.open(path); // бросит исключение, если файл не открылсяПоток в состоянии ошибки не сообщает о ней сам. Пока вы не спросили — операции просто молча пропускаются. В продакшене это выглядит как «данные иногда пропадают».
Чем платить за каждый подход
| Подход | Плюс | Минус |
|---|---|---|
Ручная проверка if (!stream) | Явно, никаких сюрпризов, легко читать | Легко забыть после очередной операции |
exceptions() | Забыть нельзя, ошибка не проглатывается | eofbit тоже может кинуть исключение при чтении до конца — ловится неожиданно |
Осторожно с failbit в exceptions() при чтении: нормальное достижение конца файла выставляет eofbit и часто одновременно failbit, что превращает штатное завершение цикла в исключение. Для чтения обычно включают только badbit.
2. flush, буферизация и потерянные данные при сбое
Запись через << не идёт на диск сразу — она копится в буфере потока, потом попадает в буфер ОС. Если процесс упадёт или его убьют между записью и сбросом, последние данные потеряются. Деструктор ofstream вызывает flush, но только при штатном разрушении объекта.
flush()выталкивает буфер C++ в ОС — но не гарантирует запись на физический диск;std::endl— это'\n'плюсflush. В цикле на миллион строк это заметно тормозит. Для просто перевода строки используйте'\n';- гарантию записи на диск даёт только системный вызов (
fsyncна POSIX,FlushFileBuffersв Windows) — стандартная библиотека такого API не предоставляет. Для критичных данных придётся спускаться к дескриптору.
Критерий выбора
Логи и отчёты, потеря последних строк которых не критична — обычная буферизованная запись, flush в конце. Данные, которые нельзя терять (транзакции, состояние) — запись во временный файл, flush, системный fsync, затем атомарное переименование через std::filesystem::rename.
3. Текстовый и бинарный режим
На Windows поток в текстовом режиме преобразует '\n' в \r\n при записи и обратно при чтении. Для бинарных данных это порча: байт 0x0A внутри картинки превратится в два байта. Открывайте бинарные файлы явно:
std::ofstream out(path, std::ios::binary);
std::ifstream in(path, std::ios::binary);Для бинарной записи используйте write() и read(), а не <</>> — операторы форматируют данные как текст:
uint32_t value = 42;
out.write(reinterpret_cast<const char*>(&value), sizeof(value));Помните, что бинарная сериализация через reinterpret_cast непереносима между платформами: порядок байтов и выравнивание структур различаются. Для обмена между системами нужен явный формат.
4. Пути и std::filesystem
С C++17 работа с путями — это std::filesystem::path, а не конкатенация строк. Она сама разбирается с разделителями и не ломается на путях с пробелами и юникодом.
#include <filesystem>
namespace fs = std::filesystem;
fs::path dir = "/var/data/reports";
fs::path file = dir / "report.txt"; // корректный разделитель на любой ОС
std::error_code ec;
fs::create_directories(dir, ec); // не бросает, пишет код в ec
if (ec) {
// каталог не создан — обработать
}У большинства функций filesystem две перегрузки: бросающая исключение и принимающая std::error_code&. В горячем пути или там, где отсутствие файла — норма, вариант с error_code быстрее и предсказуемее.
5. RAII и время жизни потока
Не вызывайте close() вручную ради «аккуратности» — деструктор сделает это сам. Ручной close() оправдан, только когда нужно закрыть файл раньше конца области видимости или проверить ошибку закрытия. Не держите открытый ofstream дольше необходимого: на некоторых ОС это блокирует файл для других процессов, а незакрытые дескрипторы накапливаются и упираются в лимит.
Чек-лист перед релизом
- Каждый открытый поток проверяется на успех открытия сразу после
openили конструктора. - После записи критичных данных состояние потока проверяется повторно — успешный
<<не значит успешную запись. - Бинарные файлы открыты с
std::ios::binary. - В горячих циклах нет
std::endl— только'\n'. - Данные, которые нельзя терять, пишутся через временный файл с атомарным переименованием.
- Пути собираются через
std::filesystem::path, а не склейкой строк. - Нет утечек дескрипторов: потоки живут ровно столько, сколько нужно.
Что дальше
После этого чек-листа стоит замерить, где именно ваш код упирается в диск: под нагрузкой узкое место обычно не в C++, а в количестве flush и fsync. Профилируйте частоту сброса буфера и число открытых дескрипторов (lsof на Linux). Если запись стала узким местом — смотрите в сторону батчинга записей и увеличения размера буфера через rdbuf()->pubsetbuf(), вызванный до открытия файла.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: std::basic_fstream — cppreference, Filesystem library — cppreference
Частые вопросы
У меня C++11, а не C++17 — что из этого работает?
Почему после успешной записи данные всё равно пропали при сбое?
Достаточно ли flush(), чтобы гарантировать запись на диск?
Нужно ли всегда вызывать close() вручную?
Почему включение exceptions() ломает мой цикл чтения?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.