Файлы в C++: fstream и потоковый ввод-вывод на примерах
Как читать и писать файлы в C++ через fstream: текстовый и бинарный режим, построчное чтение, проверка ошибок и типичные грабли с кодировкой и позицией курсора.

Файловый ввод-вывод в C++ живёт в трёх классах: std::ifstream для чтения, std::ofstream для записи и std::fstream для обоих направлений. Все три объявлены в заголовке <fstream>. Ниже — разбор на живых примерах: как открыть файл, прочитать его построчно, записать данные, отличить текстовый режим от бинарного и не наступить на классические грабли с проверкой ошибок. Код рассчитан на стандарт C++17 и новее, компилируется любым современным GCC, Clang или MSVC.
Разбираем именно потоковый API из стандартной библиотеки. C-функции fopen/fread и файловые API из C++17 <filesystem> (для работы с путями и каталогами) — отдельные темы, здесь их не трогаем.
Минимальный пример: записать и прочитать
Начнём с записи. std::ofstream при открытии по умолчанию создаёт файл (или обрезает существующий до нуля) и работает как обычный поток вывода — через <<.
#include <fstream>
#include <iostream>
#include <string>
int main() {
// Открываем файл на запись. Если файл был — его содержимое стирается.
std::ofstream out("notes.txt");
if (!out) {
std::cerr << "Не удалось открыть файл на запись\n";
return 1;
}
out << "Первая строка\n";
out << "Число: " << 42 << '\n';
// Деструктор out закроет файл сам, но можно и явно:
out.close();
return 0;
}
Обратите внимание на строку if (!out). Поток приводится к bool: если открытие не удалось (нет прав, каталога не существует, диск заполнен), проверка сработает. Пропустить её — самая частая ошибка новичков: программа молча ничего не пишет, а вы полчаса ищете баг в другом месте.
Чтение построчно
Для построчного чтения используют std::getline — не метод потока, а свободную функцию из <string>. Она читает до символа перевода строки и сам \n в результат не включает.
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ifstream in("notes.txt");
if (!in) {
std::cerr << "Файл не открылся\n";
return 1;
}
std::string line;
// getline возвращает ссылку на поток; в bool-контексте
// цикл завершится при достижении конца файла или ошибке.
while (std::getline(in, line)) {
std::cout << "[" << line << "]\n";
}
return 0;
}
Почему while (std::getline(in, line)), а не while (!in.eof())? Второй вариант — классическая ловушка. Флаг eof выставляется после неудачной попытки чтения, а не заранее. Проверяя eof() до чтения, вы обработаете последнюю строку дважды или прочитаете мусор. Проверяйте результат самой операции чтения — так корректно.
Правило одно: проверяйте не «дошли ли до конца», а «удалось ли прочитать». Поток в булевом контексте отвечает именно на второй вопрос — и отвечает честно.
Режимы открытия
Второй аргумент конструктора — флаги режима из std::ios. Их можно комбинировать через |. Самые нужные:
| Флаг | Что делает |
|---|---|
std::ios::in | Открыть на чтение (по умолчанию у ifstream) |
std::ios::out | Открыть на запись (по умолчанию у ofstream) |
std::ios::app | Дописывать в конец, не стирая содержимое |
std::ios::trunc | Обрезать файл до нуля при открытии |
std::ios::binary | Бинарный режим без преобразования переводов строк |
std::ios::ate | Сразу переместить курсор в конец файла |
Чтобы дописать строку в лог, не затирая старое:
#include <fstream>
int main() {
// app: каждая запись уходит в конец, независимо от позиции курсора.
std::ofstream log("app.log", std::ios::app);
log << "Событие произошло\n";
return 0;
}
Текстовый и бинарный режим
На Windows в текстовом режиме перевод строки \n при записи превращается в пару \r\n, а при чтении сворачивается обратно. На Linux и macOS такого преобразования нет. Если вы пишете бинарные данные — картинку, сериализованную структуру, — забытый std::ios::binary на Windows испортит файл: любой байт 0x0A внутри данных вырастет до двух байт. Для текста флаг обычно не нужен, для всего остального — обязателен.
Чтение файла целиком
Иногда нужно загрузить весь файл в одну строку — например, конфиг небольшого размера. Через строковый поток это делается в две строки:
#include <fstream>
#include <sstream>
#include <string>
std::string read_whole_file(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
return {}; // вызывающий код проверит пустоту или заведёт свой признак ошибки
}
std::ostringstream buffer;
buffer << in.rdbuf(); // перекачиваем весь буфер потока в строковый поток
return buffer.str();
}
in.rdbuf() отдаёт указатель на буфер потока, а оператор << для буфера копирует его содержимое целиком. Способ компактный, но для файла на несколько гигабайт он загрузит всё в память — тогда читайте блоками через read.
Запись и чтение бинарных данных
Для сырых байтов есть методы write и read. Оба принимают указатель на char и число байт. Указатель на любой тип приводят через reinterpret_cast.
#include <fstream>
#include <vector>
#include <cstdint>
int main() {
std::vector<std::int32_t> numbers = {1, 2, 3, 4, 5};
// Запись
std::ofstream out("data.bin", std::ios::binary);
out.write(reinterpret_cast<const char*>(numbers.data()),
numbers.size() * sizeof(std::int32_t));
out.close();
// Чтение обратно
std::ifstream in("data.bin", std::ios::binary);
std::vector<std::int32_t> loaded(5);
in.read(reinterpret_cast<char*>(loaded.data()),
loaded.size() * sizeof(std::int32_t));
// Сколько байт реально прочитано — узнаём через gcount()
std::streamsize bytes = in.gcount();
return 0;
}
Важный нюанс: бинарный файл, записанный на одной архитектуре, может не прочитаться на другой из-за порядка байт (endianness) и разного размера типов. Для обмена данными между машинами int сериализуют в фиксированный формат — например, типы фиксированной ширины из <cstdint> плюс явный порядок байт. Внутри одной программы на одной машине это не проблема.
Если не получилось
- Файл открывается, но пуст, хотя вы в него писали. Данные остались в буфере и не сброшены на диск, потому что поток не закрыт и не завершилась программа. Вызовите
out.flush()или дождитесь закрытия потока (деструктор сбросит буфер). - Последняя строка обрабатывается дважды или читается пустая. В цикле стоит
while (!in.eof()). Замените наwhile (std::getline(in, line)). - На Windows бинарный файл длиннее ожидаемого. Забыт флаг
std::ios::binary— байты0x0Aразмножились. Добавьте флаг в конструктор. - Кириллица превращается в «кракозябры». C++-потоки не перекодируют текст. Файл в UTF-8 читается как последовательность байт; проблема на стороне консоли или редактора, а не потока. Смотрите кодировку вывода терминала.
- Относительный путь не находит файл. Путь считается от рабочего каталога процесса, а не от места, где лежит исполняемый файл. Запустите с абсолютным путём или проверьте, откуда стартует программа.
Что смотреть дальше
Освоив fstream, переходите к трём вещам. Первое — <filesystem> из C++17: проверка существования файла, обход каталогов, работа с путями кроссплатформенно. Второе — методы позиционирования seekg, seekp, tellg, tellp, когда нужно читать или писать не с начала. Третье — обработка ошибок через exceptions(), если вы предпочитаете исключения ручной проверке флагов. Для боевого кода включите проверку каждого открытия файла в отдельную функцию — так вы не забудете её ни в одном месте.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: std::basic_fstream — cppreference.com, std::getline — cppreference.com
Частые вопросы
Чем отличается ifstream от fstream, если fstream умеет и то и другое?
Нужно ли вызывать close() вручную?
Почему файл пустой, хотя программа отработала без ошибок?
У меня старый компилятор без поддержки C++17. Примеры сработают?
Как правильно проверять ошибки: через if(!поток) или через исключения?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.