Файлы и потоки: текст и бинарный режим

Файлы — это долговременное хранилище данных. В C++ есть два принципиально разных способа записи: текстовый (человекочитаемый) и бинарный (компактный). Здесь мы разберём оба и наглядно увидим разницу.

ifstream / ofstream getline fwrite / fread seekg / seekp

Потоки: единая абстракция ввода-вывода

В C++ ввод-вывод строится на потоках (streams) — объектах, через которые данные «текут» от источника к приёмнику. Экран, клавиатура, файл, строка в памяти — всё это может быть потоком.

ios_base ios istream iostream ostream ifstream файл — чтение fstream файл — чтение + запись ofstream файл — запись istringstream память ostring- stream Все потоки имеют одинаковый интерфейс (операторы << и >>, getline, seek и др.) Один и тот же код может читать и с клавиатуры, и из файла, и из строки в памяти
Ключевое преимущество потоков. Функция readData(istream& in) может принимать и cin, и ifstream, и istringstream. Пишете логику один раз — работает с любым источником.

Демонстрация 1: текстовый файл — запись и чтение

Введите текст, сохраните его «в файл» и прочитайте обратно построчно. Так работают ofstream и ifstream в обычном режиме.

Шаг 1: напишите содержимое

Файл пока не записан

📄 Файл на диске

// содержимое появится после записи
размер: 0 байт строк: 0 позиция чтения: 0

💻 Консоль программы

// здесь будут появляться операции
состояние: —
запись и чтение текстового файла
#include <fstream>
#include <string>
using namespace std;

int main() {
    // ЗАПИСЬ
    {
        ofstream out("data.txt");           // открыт на запись
        out << "Привет, файл!" << endl;
        out << "Это вторая строка." << endl;
    }   // out.~ofstream() автоматически закрывает файл

    // ЧТЕНИЕ
    {
        ifstream in("data.txt");             // открыт на чтение
        if (!in) { cerr << "Не удалось открыть" << endl; return 1; }

        string line;
        int lineNum = 1;
        while (getline(in, line)) {          // построчно
            cout << lineNum++ << ": " << line << endl;
        }
    }
}
Что важно заметить. При закрытии файла (в деструкторе ofstream) данные принудительно сбрасываются на диск. Если забыть закрыть — возможна потеря последних записей при аварийном завершении. RAII делает это автоматически.

Режимы открытия файла

Второй аргумент конструктора ofstream задаёт режим. По умолчанию ofstream открывается в ios::out, ifstream — в ios::in. Режимы можно комбинировать через |.

Интерактивно: что произойдёт с существующим файлом?

Содержимое файла до открытия:

Первая строка
Вторая строка
Третья строка

Содержимое после открытия в выбранном режиме:

// выберите режим в сетке выше
$ Выберите режим, чтобы увидеть результат

Демонстрация 2: текстовый vs бинарный файл

Запишем одну и ту же структуру двумя способами и сравним размер, читаемость и скорость. Разница станет видна сразу.

структура для сохранения
struct Product {
    int id;            // 4 байта
    char name[16];     // 16 байт
    double price;      // 8 байт
};
id = 42 name = "Apple" price = 3.14

📄 Текстовый файл

Нажмите «Записать оба»
Размер:—
Читаемость:глазами
Парсинг:нужен

📦 Бинарный файл

Нажмите «Записать оба»
Размер:—
Читаемость:нечитаемо
Парсинг:не нужен
$ Нажмите «Записать оба»
текстовая запись
ofstream out("product.txt");
out << p.id << " "
    << p.name << " "
    << p.price << endl;

// Файл:
// 42 Apple 3.14\n
бинарная запись
ofstream out("product.dat",
             ios::binary);
out.write((char*)&p, sizeof(p));

// 28 байт «как есть» в памяти
// Без разделителей, без текста
Осторожно с бинарной записью структур. Между полями структуры могут быть «дырки» (padding) для выравнивания. Размер на разных платформах может отличаться. Для переносимых форматов используйте сериализацию вручную (запись каждого поля отдельно), либо стандартные форматы вроде JSON / Protobuf.

Демонстрация 3: позиционирование в файле

У каждого файлового потока есть «голова чтения-записи». Её можно перемещать в любое место — это позволяет читать и писать не последовательно, а в произвольном порядке (прямой доступ).

функции позиционирования
in.seekg(offset, origin);    // для чтения (g = get)
out.seekp(offset, origin);   // для записи (p = put)

// origin задаёт точку отсчёта:
//   ios::beg  — от начала файла
//   ios::cur  — от текущей позиции
//   ios::end  — от конца файла

in.tellg();    // текущая позиция чтения
out.tellp();   // текущая позиция записи

Интерактивно: перемещение по файлу

Файл содержит 8 чисел. Нажимайте кнопки, чтобы переместить «голову» и прочитать значение.

позиция: 0 (байт) значение: —
$ Нажмите на любую кнопку seekg
Как это работает. Файл — это просто последовательность байтов с нумерацией от 0. Позиция — номер байта, с которого начнётся следующая операция чтения или записи. seekg не читает данные — только перемещает «голову».

Проверка ошибок при работе с файлами

Файл может не открыться (не существует, нет прав, диск занят). Всегда проверяйте состояние потока перед использованием.

четыре способа проверить состояние
ifstream in("data.txt");

if (!in)              { /* файл не открылся */ }
if (in.fail())        { /* ошибка открытия или операции */ }
if (in.bad())         { /* серьёзная ошибка (потеря потока) */ }
if (in.eof())         { /* достигнут конец файла */ }

// Хорошая практика: явная проверка сразу после открытия
ifstream in("data.txt");
if (!in.is_open()) {
    cerr << "Не удалось открыть файл" << endl;
    return 1;
}
Особенность while (!in.eof()). Классическая ошибка — читать в цикле с проверкой eof(). Флаг eof выставляется только после попытки чтения за концом файла. Правильно — проверять результат операции чтения: while (getline(in, line)).

Лучшие практики

1. Всегда проверяйте открытие файла

правильно
ifstream in("data.txt");
if (!in) {
    cerr << "Файл не найден" << endl;
    return 1;
}

2. Используйте RAII — файл закроется сам

ifstream и ofstream закрывают файл в деструкторе. Не нужно вызывать close() вручную — и не нужно бояться, что что-то останется открытым.

3. Читайте построчно через getline

Для текстовых файлов это самый естественный способ. Одна строка — одна итерация цикла. Если нужно разбить строку на слова — используйте istringstream.

разбор строки на слова
string line;
while (getline(in, line)) {
    istringstream iss(line);
    string word;
    while (iss >> word) {
        // обрабатываем слово
    }
}

4. Для бинарных данных — фиксированный формат

Не записывайте структуры целиком через write((char*)&p, sizeof(p)), если файл должен читаться на другой платформе. Записывайте каждое поле отдельно: out.write((char*)&p.id, sizeof(p.id)); и так далее. Это дольше, но переносимо.

Антипаттерн: писать всё в текстовый файл

Текстовый формат удобен для логов и конфигов. Но для больших массивов чисел он неэффективен: каждое число занимает несколько байт, плюс разделители. Бинарный формат даёт сжатие в 2–5 раз и более быструю запись-чтение. Выбирайте по задаче.

Антипаттерн: забывать ios::binary

Если открыть файл без ios::binary и записать в него байты, ОС может вставить дополнительные символы (перевод строки \n → \r\n в Windows) или изменить поток. Файл станет «порченым» при чтении на другой платформе.

Шпаргалка

ЗадачаКод
Открыть файл на чтениеifstream in("file.txt");
Открыть файл на записьofstream out("file.txt");
Открыть на чтение и записьfstream f("file.txt", ios::in | ios::out);
Открыть в бинарном режимеofstream out("file.dat", ios::binary);
Дописать в конецofstream out("file.txt", ios::app);
Проверить, открылся лиif (!in) { ... }
Прочитать строкуgetline(in, line);
Прочитать одно слово/числоin >> x;
Записать текстout << "text" << endl;
Записать бинарные данныеout.write((char*)&x, sizeof(x));
Прочитать бинарные данныеin.read((char*)&x, sizeof(x));
Переместить позицию чтенияin.seekg(offset, ios::beg);
Получить текущую позициюauto pos = in.tellg();

Задание

  1. Создайте текстовый файл, запишите в него три строки. Прочитайте его через ifstream и выведите каждую строку с номером.
  2. Посчитайте количество слов в текстовом файле. Используйте ifstream и istringstream.
  3. Создайте структуру Student { int id; char name[20]; double gpa; }. Запишите массив из 5 студентов в текстовый файл, затем прочитайте обратно.
  4. То же самое — в бинарный файл. Сравните размеры файлов.
  5. Используя seekg, прочитайте из бинарного файла только третьего студента, не читая первых двух.
  6. Реализуйте программу «дневник»: пользователь вводит строки, они дописываются в файл diary.txt в режиме ios::app. При запуске программа выводит все прошлые записи.
  7. Скопируйте содержимое одного текстового файла в другой. Используйте построчное чтение.
  8. Что произойдёт, если открыть файл в режиме ios::out второй раз? А в режиме ios::app? Проверьте экспериментально.