C++

Робота з файлами: C++-стиль (fstream)

Сучасний об'єктно-орієнтований підхід до роботи з файлами в C++ через бібліотеку fstream. Класи ifstream, ofstream, fstream, режими відкриття, потокові оператори << та >>, рядкове читання, позиціонування та бінарний ввід/вивід.

Робота з файлами: C++-стиль (fstream)

Від stdio.h до fstream: чому з'явився новий підхід

У попередньому розділі ми детально вивчили класичний C-підхід до роботи з файлами через бібліотеку <cstdio>. Він ефективний, переносний і повсюдно підтримуваний — але є суттєвим анахронізмом у контексті сучасного C++.

Найголовніший його недолік: відсутність типобезпеки. Функція fscanf(file, "%d", &value) приймає довільний вказівник і ніяк не гарантує, що тип змінної відповідає специфікатору формату. Помилка на кшталт fscanf(file, "%f", &intVar) — тихий UB (невизначена поведінка), яку компілятор не зловить.

C++ пропонує принципово інший підхід, побудований на тих самих концепціях потоків (streams), що й для консольного вводу/виводу. Ви вже знайомі з std::cin і std::coutifstream, ofstream і fstream є їхніми файловими аналогами, з тими самими операторами та методами.

Переваги C++-підходу

  • Типобезпека: оператори << і >> завжди знають тип даних під час компіляції
  • RAII: деструктор автоматично закриває файл при виході зі scope
  • Єдиний синтаксис: той самий <</>>, що і з cout/cin
  • Обробка помилок: стан потоку через good(), fail(), eof(), bad()

Коли ще використовують C-стиль

  • Бінарні файли з точним контролем байтів (fread/fwrite)
  • Взаємодія з legacy-кодом або C-бібліотеками
  • Максимальна продуктивність (нижчий overhead)
  • Платформно-специфічний код (POSIX API)

Ієрархія класів файлового вводу/виводу

Бібліотека <fstream> побудована на вже знайомій вам ієрархії потокових класів:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam classBackgroundColor #f8f9fa
skinparam classBorderColor #6c757d

class ios_base

class ios {
  +good()
  +fail()
  +eof()
  +bad()
  +clear()
}

class istream {
  +operator>>()
  +get()
  +getline()
  +read()
  +tellg()
  +seekg()
}

class ostream {
  +operator<<()
  +put()
  +write()
  +tellp()
  +seekp()
  +flush()
}

class iostream

class ifstream {
  +open()
  +close()
  +is_open()
}

class ofstream {
  +open()
  +close()
  +is_open()
}

class fstream {
  +open()
  +close()
  +is_open()
}

ios_base <|-- ios
ios <|-- istream
ios <|-- ostream
istream <|-- iostream
ostream <|-- iostream
istream <|-- ifstream
ostream <|-- ofstream
iostream <|-- fstream
@enduml

Ця ієрархія важлива: оскільки ifstream успадковує istream, він підтримує всі методи та оператори, доступні для std::cin. Аналогічно ofstream підтримує все, що є у std::cout. Це робить перехід між файловим і консольним вводом/виводом абсолютно прозорим.

Для підключення бібліотеки:

Main.cpp
#include <fstream>  // ifstream, ofstream, fstream
// Додатково може знадобитися:
#include <string>   // std::string, std::getline
#include <sstream>  // std::istringstream, std::ostringstream
На відміну від C-підходу, тут не потрібно підключати <cstdio>. Бібліотека <fstream> є самодостатньою і включає всю необхідну інфраструктуру потоків.

Клас ofstream: запис у файл

Клас std::ofstream (output file stream) призначений для запису даних у файл. Він є точним файловим аналогом std::cout.

Відкриття через конструктор

Найпростіший спосіб відкрити файл — передати його ім'я в конструктор:

std::ofstream outputFile("filename.txt");
std::ofstream outputFile("filename.txt", mode);  // з режимом

Якщо файл не існує — він буде створений. Якщо існує — за замовчуванням очищений (режим ios::out).

Запис через оператор <<

Як і cout, ofstream підтримує оператор вставки << для запису будь-яких типів даних:

WriteBasic.cpp
#include <fstream>
#include <string>

int main()
{
    std::ofstream outputFile("notes.txt");

    // Перевірка відкриття (метод is_open або неявне перетворення до bool)
    if (!outputFile)
    {
        return 1;
    }

    // Запис різних типів — синтаксис ідентичний cout
    outputFile << "Ім\'я: " << "Іван Петренко" << "\n";
    outputFile << "Вік: "   << 20               << "\n";
    outputFile << "GPA: "   << 4.75             << "\n";
    outputFile << "Активний: " << true          << "\n";

    // Файл закривається автоматично при виході зі scope (RAII)
    return 0;
}
cat notes.txt
$ g++ -std=c++17 WriteBasic.cpp -o WriteBasic && ./WriteBasic
$ cat notes.txt
Ім'я: Іван Петренко
Вік: 20
GPA: 4.75
Активний: 1

Явне закриття: метод close()

Попри RAII, іноді потрібно закрити файл явно — наприклад, щоб одразу після закриття відкрити його для читання:

CloseExplicit.cpp
#include <fstream>
#include <iostream>

int main()
{
    std::ofstream file("data.txt");

    if (!file.is_open())
    {
        std::cerr << "Помилка відкриття файлу\n";
        return 1;
    }

    file << "Hello, file!\n";

    // Явно закриваємо — RAII все одно спрацює при виході,
    // але явний close() дозволяє перевірити помилку запису
    file.close();

    if (file.fail())
    {
        std::cerr << "Помилка під час закриття файлу\n";
        return 1;
    }

    std::cout << "Файл успішно записано і закрито\n";
    return 0;
}
Метод is_open() повертає true, якщо потік зараз пов'язаний з відкритим файлом. Неявне перетворення if (!file) — скорочена перевірка стану: вона перевіряє комбінацію прапорців failbit і badbit, що ширше за is_open(). Рекомендується використовувати is_open() після відкриття і if (!file) після операцій вводу/виводу.

Клас ifstream: читання з файлу

Клас std::ifstream (input file stream) призначений для читання даних із файлу. Він є файловим аналогом std::cin.

Читання через оператор >>

Оператор >> читає форматовані дані, ігноруючи пробіли, табуляцію і символи нового рядка — рівно як і std::cin:

ReadBasic.cpp
#include <fstream>
#include <iostream>
#include <string>

int main()
{
    // Спочатку запишемо тестовий файл
    {
        std::ofstream outFile("numbers.txt");
        outFile << "10 25 37 42 58\n";
    }

    // Відкриваємо для читання
    std::ifstream inputFile("numbers.txt");

    if (!inputFile.is_open())
    {
        std::cerr << "Файл не знайдено\n";
        return 1;
    }

    int value;
    int sum   = 0;
    int count = 0;

    // Читання у циклі — оператор >> повертає потік,
    // який неявно перетворюється до bool (false при EOF або помилці)
    while (inputFile >> value)
    {
        std::cout << "Прочитано: " << value << "\n";
        sum   += value;
        count++;
    }

    std::cout << "Сума: " << sum << ", Кількість: " << count << "\n";
    std::cout << "Середнє: " << static_cast<double>(sum) / count << "\n";

    return 0;
}
./ReadBasic
$ ./ReadBasic
Прочитано: 10
Прочитано: 25
Прочитано: 37
Прочитано: 42
Прочитано: 58
Сума: 172, Кількість: 5
Середнє: 34.4
Execution finished with exit code 0.
Ключова перевага над fscanf: умова while (inputFile >> value) є типобезпечною і автоматично зупиняється при EOF або при помилці формату. Нема потреби перевіряти повернене значення вручну.

Читання рядків: std::getline

Оператор >> зупиняється на пробілах. Для читання цілих рядків (включно з пробілами) використовується вільна функція std::getline():

bool getline(std::istream& input, std::string& line);
bool getline(std::istream& input, std::string& line, char delimiter);

Читає символи до delimiter (за замовчуванням — '\n'), зберігає в line без символу-роздільника. Повертає посилання на потік (перетворюється на false при EOF або помилці).

ReadLines.cpp
#include <fstream>
#include <iostream>
#include <string>

int main()
{
    // Записуємо тестовий файл з реченнями
    {
        std::ofstream outFile("poem.txt");
        outFile << "Реве та стогне Дніпр широкий,\n";
        outFile << "Сердитий вітер завива,\n";
        outFile << "Додолу верби гне високі,\n";
        outFile << "Горами хвилю підійма.\n";
    }

    std::ifstream inputFile("poem.txt");
    if (!inputFile.is_open())
    {
        std::cerr << "Файл не знайдено\n";
        return 1;
    }

    std::string line;
    int         lineNum = 0;

    while (std::getline(inputFile, line))
    {
        lineNum++;
        std::cout << lineNum << ": " << line << "\n";
    }

    std::cout << "\nВсього рядків: " << lineNum << "\n";

    return 0;
}
./ReadLines
$ ./ReadLines
1: Реве та стогне Дніпр широкий,
2: Сердитий вітер завива,
3: Додолу верби гне високі,
4: Горами хвилю підійма.
Всього рядків: 4
Execution finished with exit code 0.

Режими відкриття файлів

На відміну від C-стилю (де режим задається рядком: "r", "wb" і т.д.), у C++ режим задається через константи класу std::ios. Вони можуть комбінуватися побітовим АБО (|).

ПрапорецьЗначення
ios::inВідкрити для читання (за замовчуванням для ifstream)
ios::outВідкрити для запису (за замовчуванням для ofstream)
ios::appДозапис у кінець файлу
ios::ateПерейти в кінець після відкриття
ios::truncОчистити файл при відкритті (за замовчуванням для ios::out)
ios::binaryВідкрити в бінарному режимі

Режим дозапису: ios::app

AppendMode.cpp
#include <fstream>
#include <iostream>

int main()
{
    // Перший запуск: створюємо файл
    {
        std::ofstream logFile("app.log");
        logFile << "[2024-01-01] Програма запущена\n";
    }

    // Другий запуск: ДОЗАПИСУЄМО, не перезаписуємо
    {
        std::ofstream logFile("app.log", std::ios::app);
        logFile << "[2024-01-01] Операція виконана\n";
        logFile << "[2024-01-01] Програма завершена\n";
    }

    // Читаємо весь файл
    std::ifstream inFile("app.log");
    std::string   line;
    while (std::getline(inFile, line))
    {
        std::cout << line << "\n";
    }

    return 0;
}
./AppendMode
$ ./AppendMode
[2024-01-01] Програма запущена
[2024-01-01] Операція виконана
[2024-01-01] Програма завершена
Execution finished with exit code 0.

Режим оновлення: ios::in | ios::out

fstream з комбінацією ios::in | ios::out відкриває існуючий файл для одночасного читання і запису, не очищаючи його:

ReadWriteMode.cpp
#include <fstream>
#include <iostream>

int main()
{
    // Спочатку створюємо файл
    {
        std::ofstream outFile("data.txt");
        outFile << "Hello World\n";
        outFile << "C++ Files\n";
    }

    // Відкриваємо для читання і запису
    std::fstream rwFile("data.txt", std::ios::in | std::ios::out);

    if (!rwFile.is_open())
    {
        std::cerr << "Файл не знайдено\n";
        return 1;
    }

    // Читаємо перший рядок
    std::string firstLine;
    std::getline(rwFile, firstLine);
    std::cout << "Перший рядок: " << firstLine << "\n";

    // Файловий покажчик тепер після першого рядка
    // Записуємо в поточну позицію
    rwFile << "INSERTED LINE\n";

    // Перечитуємо весь файл
    rwFile.seekg(0);
    std::string line;
    std::cout << "\nВміст після модифікації:\n";
    while (std::getline(rwFile, line))
    {
        std::cout << line << "\n";
    }

    return 0;
}
При перемиканні між читанням і записом у fstreamобов'язково виконайте seekg() або seekp(). Стандарт C++ вимагає цього між будь-якими переходами від читання до запису і навпаки — інакше поведінка невизначена.

Таблиця відповідності C-режимів і C++-режимів

C-рядокC++ прапорці
"r"ios::in
"w"ios::out або ios::out | ios::trunc
"a"ios::app
"r+"ios::in | ios::out
"w+"ios::in | ios::out | ios::trunc
"rb"ios::in | ios::binary
"wb"ios::out | ios::binary
"ab"ios::app | ios::binary
"r+b"ios::in | ios::out | ios::binary

Метод open() та повторне відкриття файлів

Окрім конструктора, файл можна відкрити явно через метод open(). Це корисно, коли ім'я файлу невідоме до виконання, або коли потрібно перевикористати об'єкт потоку:

OpenMethod.cpp
#include <fstream>
#include <iostream>
#include <string>

int main()
{
    std::ifstream inputFile;  // Поки не відкритий

    std::string fileName;
    std::cout << "Введіть ім'я файлу: ";
    std::cin >> fileName;

    // Відкриваємо пізніше
    inputFile.open(fileName);

    if (!inputFile.is_open())
    {
        std::cerr << "Файл '" << fileName << "' не знайдено\n";
        return 1;
    }

    std::string line;
    int         lineCount = 0;

    while (std::getline(inputFile, line))
    {
        lineCount++;
    }

    inputFile.close();  // Явно закриваємо

    std::cout << "Рядків у файлі: " << lineCount << "\n";

    // Повторно відкриваємо той самий об'єкт
    inputFile.open(fileName);
    if (inputFile.is_open())
    {
        std::cout << "Перший рядок: ";
        std::getline(inputFile, line);
        std::cout << line << "\n";
    }

    return 0;
}
Якщо об'єкт ifstream вже був відкритий і ви викликаєте open() без попереднього close(), операція завершиться невдачею і прапорець failbit буде встановлено. Завжди закривайте файл перед повторним відкриттям.

Стан потоку та обробка помилок

Кожен потік має набір прапорців стану (state flags), що відображають його поточний стан:

goodbit
bool good()
Потік у нормальному стані. Усі прапорці помилок скинуті. Операції вводу/виводу виконуються коректно.
eofbit
bool eof()
Досягнуто кінця файлу. Не є помилкою саме по собі, але подальше читання неможливе.
failbit
bool fail()
Операція не вдалася (наприклад, неможливо прочитати ціле число через невірний формат або EOF). Дані залишаються у потоці.
badbit
bool bad()
Критична помилка потоку (наприклад, фізична помилка читання з диска). Потік непридатний до використання.

Перевірка і скидання стану

StreamState.cpp
#include <fstream>
#include <iostream>
#include <string>

int main()
{
    // Підготуємо файл з числами і рядком
    {
        std::ofstream outFile("mixed.txt");
        outFile << "42 17 hello 99\n";
    }

    std::ifstream inFile("mixed.txt");
    int           value;

    // Читаємо числа, поки не помилка (рядок "hello" не є int)
    while (inFile >> value)
    {
        std::cout << "Прочитано: " << value << "\n";
    }

    // Аналізуємо стан
    std::cout << "\nСтан після читання:\n";
    std::cout << "  good(): " << inFile.good() << "\n";
    std::cout << "  eof():  " << inFile.eof()  << "\n";
    std::cout << "  fail(): " << inFile.fail() << "\n";
    std::cout << "  bad():  " << inFile.bad()  << "\n";

    // Скидаємо failbit — це дозволяє читати далі
    inFile.clear();

    // Читаємо невдале слово як рядок
    std::string word;
    inFile >> word;
    std::cout << "\nНевдале слово було: \"" << word << "\"\n";

    // Продовжуємо читання числа
    inFile >> value;
    std::cout << "Наступне число: " << value << "\n";

    return 0;
}
./StreamState
$ ./StreamState
Прочитано: 42
Прочитано: 17
Стан після читання:
good(): 0
eof(): 0
fail(): 1
bad(): 0
Невдале слово було: "hello"
Наступне число: 99
Execution finished with exit code 0.

Виключення з потоків

За бажанням можна налаштувати потік так, щоб він кидав виключення замість встановлення прапорців. Це корисно у коді, де виключення вже є основним механізмом обробки помилок:

StreamExceptions.cpp
#include <fstream>
#include <iostream>
#include <stdexcept>

int main()
{
    std::ifstream inFile;

    // Налаштовуємо: кидати виключення при failbit і badbit
    inFile.exceptions(std::ios::failbit | std::ios::badbit);

    try
    {
        inFile.open("nonexistent.txt");

        // До цього рядка ми не дійдемо — open() кине виключення
        std::string line;
        std::getline(inFile, line);
    }
    catch (const std::ios_base::failure& e)
    {
        std::cerr << "Помилка файлового потоку: " << e.what() << "\n";
    }

    return 0;
}
Виключення з потоків мають деякі нюанси: при досягненні EOF разом із failbit (якщо читання не вдалося через кінець файлу) також може бути кинуто виключення. Тому налаштування exceptions() рекомендується лише для відкриття файлів, а не для операцій читання в циклі.

Робота зі структурами: читання і запис

Розглянемо повноцінний приклад: запис і читання масиву структур-студентів у текстовий файл.

StudentIO.cpp
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

struct Student
{
    std::string name;
    int         age;
    double      gpa;
};

// Записати вектор студентів у файл (формат CSV)
void saveStudents(const std::string& filename, const std::vector<Student>& students)
{
    std::ofstream outFile(filename);

    if (!outFile.is_open())
    {
        std::cerr << "Помилка: не вдалося відкрити файл для запису\n";
        return;
    }

    for (const auto& s : students)
    {
        // Формат: ім'я,вік,gpa
        outFile << s.name << "," << s.age << "," << s.gpa << "\n";
    }
}

// Зчитати студентів з CSV-файлу
std::vector<Student> loadStudents(const std::string& filename)
{
    std::vector<Student> result;
    std::ifstream        inFile(filename);

    if (!inFile.is_open())
    {
        std::cerr << "Помилка: файл не знайдено\n";
        return result;
    }

    std::string line;
    while (std::getline(inFile, line))
    {
        Student s;

        // Парсимо CSV-рядок через stringstream
        std::istringstream ss(line);
        std::string        ageStr;
        std::string        gpaStr;

        std::getline(ss, s.name,   ',');
        std::getline(ss, ageStr,   ',');
        std::getline(ss, gpaStr,   ',');

        s.age = std::stoi(ageStr);
        s.gpa = std::stod(gpaStr);

        result.push_back(s);
    }

    return result;
}

int main()
{
    std::vector<Student> students = {
        { "Іван Петренко",   20, 4.5 },
        { "Олена Коваль",    19, 4.8 },
        { "Михайло Бойко",   21, 3.9 },
        { "Анна Савченко",   20, 4.2 }
    };

    const std::string fileName = "students.csv";
    saveStudents(fileName, students);
    std::cout << "Збережено " << students.size() << " студентів у " << fileName << "\n";

    auto loaded = loadStudents(fileName);
    std::cout << "\nЗавантажено " << loaded.size() << " студентів:\n";

    for (const auto& s : loaded)
    {
        std::cout << "  " << s.name
                  << " | вік: " << s.age
                  << " | GPA: " << s.gpa << "\n";
    }

    return 0;
}
./StudentIO
$ ./StudentIO
Збережено 4 студентів у students.csv
Завантажено 4 студентів:
Іван Петренко | вік: 20 | GPA: 4.5
Олена Коваль | вік: 19 | GPA: 4.8
Михайло Бойко | вік: 21 | GPA: 3.9
Анна Савченко | вік: 20 | GPA: 4.2
Execution finished with exit code 0.
Зверніть на включення <sstream> для std::istringstream. Це потоковий клас, що дозволяє читати дані зі звичайного рядка std::string так само, як з файлу або консолі. Це надзвичайно зручно для парсингу рядків, розділених роздільниками.

Буферизований вивід та очищення буфера

Вивід у C++ є буферизованим за тими самими причинами, що й у C-стилі: побайтовий запис на диск був би катастрофічно повільним. Клас ofstream підтримує два способи очищення буфера:

// Спосіб 1: Маніпулятор std::flush
file << "важливі дані" << std::flush;

// Спосіб 2: Метод flush()
file.flush();
Небезпека std::endl: маніпулятор std::endl еквівалентний '\n' << std::flush. Це означає, що кожен std::endl очищає буфер — а це може бути на порядок повільніше, ніж просто '\n'. У циклах, що виводять тисячі рядків, надлишковий std::endl суттєво знижує продуктивність. Завжди використовуйте '\n' для рядків, що не вимагають негайного запису.

Приклад: лог-файл із гарантованим записом

SafeLogger.cpp
#include <fstream>
#include <iostream>
#include <string>

class Logger
{
public:
    explicit Logger(const std::string& filename)
        : m_file(filename, std::ios::app)
    {
        if (!m_file.is_open())
        {
            throw std::runtime_error("Не вдалося відкрити лог-файл: " + filename);
        }
    }

    void log(const std::string& message)
    {
        m_file << message << "\n";
        m_file.flush();  // Гарантуємо запис на диск
    }

    // Деструктор автоматично закриє файл (RAII)

private:
    std::ofstream m_file;
};

int main()
{
    Logger logger("system.log");

    logger.log("[INFO] Програма запущена");
    logger.log("[INFO] Ініціалізація завершена");
    logger.log("[WARN] Низький рівень пам'яті");
    logger.log("[INFO] Програма завершена");

    std::cout << "Записи успішно збережено у system.log\n";
    return 0;
}
Клас Logger у прикладі вище є прикладом RAII (Resource Acquisition Is Initialization) — ідіоми, що є основою сучасного C++. Ресурс (файл) захоплюється в конструкторі і автоматично звільняється в деструкторі. Навіть якщо виникне виключення, деструктор гарантовано виконається і файл буде коректно закрито.

Позиціонування у файлі: seekg, seekp, tellg, tellp

Клас fstream підтримує довільний доступ (random access) до файлу. На відміну від C-стилю (де є один покажчик позиції), у C++ для читання і запису є окремі покажчики:

  • seekg / tellg — покажчик читання (від get)
  • seekp / tellp — покажчик запису (від put)

Синтаксис аналогічний fseek/ftell:

// Встановити покажчик
stream.seekg(offset, direction);   // для читання
stream.seekp(offset, direction);   // для запису

// Отримати поточну позицію
std::streampos posRead  = stream.tellg();
std::streampos posWrite = stream.tellp();

Напрямки: std::ios::beg, std::ios::cur, std::ios::end.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "Файл" {
  card "[ B | y | t | e | 0 | B | y | t | e | 1 | B | y | t | e | 2 ]" as FileBytes
}

label "get pointer (seekg/tellg)\nReadPosition" as GP
label "put pointer (seekp/tellp)\nWritePosition" as PP

GP --> FileBytes : "зчитує звідси"
PP --> FileBytes : "записує сюди"

note bottom of FileBytes
  ios::beg — від початку
  ios::cur — від поточної позиції
  ios::end — від кінця
end note
@enduml

Визначення розміру файлу

FileSize.cpp
#include <fstream>
#include <iostream>

std::streamsize getFileSize(const std::string& filename)
{
    std::ifstream file(filename, std::ios::binary | std::ios::ate);

    if (!file.is_open())
    {
        return -1;
    }

    // ios::ate вже перемістив покажчик у кінець
    // tellg() повертає поточну позицію = розмір файлу
    return file.tellg();
}

int main()
{
    std::streamsize size = getFileSize("students.csv");

    if (size == -1)
    {
        std::cerr << "Файл не знайдено\n";
        return 1;
    }

    std::cout << "Розмір students.csv: " << size << " байтів\n";
    return 0;
}
./FileSize
$ ./FileSize
Розмір students.csv: 99 байтів
Execution finished with exit code 0.

Читання всього файлу в рядок

Комбінація seekg(0, ios::end) + tellg() + seekg(0, ios::beg) — класичний прийом завантаження всього файлу в std::string одним рядком:

ReadAll.cpp
#include <fstream>
#include <iostream>
#include <string>

std::string readFileToString(const std::string& filename)
{
    std::ifstream file(filename, std::ios::binary);

    if (!file.is_open())
    {
        return "";
    }

    // Визначаємо розмір
    file.seekg(0, std::ios::end);
    std::streamsize fileSize = file.tellg();
    file.seekg(0, std::ios::beg);

    // Читаємо весь файл одним блоком
    std::string content(static_cast<size_t>(fileSize), '\0');
    file.read(content.data(), fileSize);

    return content;
}

int main()
{
    std::string text = readFileToString("poem.txt");

    if (text.empty())
    {
        std::cerr << "Файл не знайдено або порожній\n";
        return 1;
    }

    std::cout << "=== Вміст файлу ===\n" << text;
    std::cout << "=== Розмір: " << text.size() << " байтів ===\n";

    return 0;
}
./ReadAll
$ ./ReadAll
=== Вміст файлу ===
Реве та стогне Дніпр широкий,
Сердитий вітер завива,
Додолу верби гне високі,
Горами хвилю підійма.
=== Розмір: 115 байтів ===
Execution finished with exit code 0.
Метод file.read(content.data(), fileSize) читає рівно fileSize байтів у масив символів рядка. Це найшвидший спосіб завантажити файл: одна системна операція читання замість численних викликів getline(). Після цього content.size() дорівнює реальному розміру файлу.

Бінарні файли: методи read() і write()

Клас fstream підтримує бінарне читання і запис через методи read() і write(), які є аналогами C-функцій fread() і fwrite():

// Запис n байтів із буфера
stream.write(reinterpret_cast<const char*>(ptr), n);

// Читання n байтів у буфер
stream.read(reinterpret_cast<char*>(ptr), n);
Зверніть на reinterpret_cast<char*>: методи read()/write() працюють із сирими байтами (char*). Для передачі вказівника на структуру або масив числових типів необхідний reinterpret_cast. Це нормально для бінарного вводу/виводу — ми свідомо інтерпретуємо пам'ять як послідовність байтів.

Запис і читання масиву чисел

BinaryNumbers.cpp
#include <fstream>
#include <iostream>

int main()
{
    const int COUNT = 5;
    int writeData[COUNT] = { 100, 200, 300, 400, 500 };

    // --- Запис ---
    {
        std::ofstream outFile("numbers.bin", std::ios::binary);

        if (!outFile.is_open())
        {
            std::cerr << "Помилка запису\n";
            return 1;
        }

        // Записуємо весь масив одним блоком
        outFile.write(reinterpret_cast<const char*>(writeData),
                      COUNT * sizeof(int));
    }

    // --- Читання ---
    int readData[COUNT] = {};
    {
        std::ifstream inFile("numbers.bin", std::ios::binary);

        if (!inFile.is_open())
        {
            std::cerr << "Помилка читання\n";
            return 1;
        }

        inFile.read(reinterpret_cast<char*>(readData),
                    COUNT * sizeof(int));

        // Перевіряємо, скільки байтів реально прочитано
        std::streamsize bytesRead = inFile.gcount();
        std::cout << "Прочитано байтів: " << bytesRead << "\n";
    }

    // Виводимо прочитані дані
    std::cout << "Числа: ";
    for (int i = 0; i < COUNT; i++)
    {
        std::cout << readData[i] << " ";
    }
    std::cout << "\n";

    return 0;
}
./BinaryNumbers
$ ./BinaryNumbers
Прочитано байтів: 20
Числа: 100 200 300 400 500
Execution finished with exit code 0.

Запис структур у бінарний файл

Найпотужніша особливість бінарного вводу/виводу — можливість зберегти структуру одним викликом:

BinaryStruct.cpp
#include <fstream>
#include <iostream>
#include <cstring>

struct StudentRecord
{
    char   name[50];
    int    age;
    double gpa;
};

int main()
{
    StudentRecord writeRecords[3];

    std::strncpy(writeRecords[0].name, "Petrenko Ivan",  49);
    writeRecords[0].age = 20;  writeRecords[0].gpa = 4.5;

    std::strncpy(writeRecords[1].name, "Koval Olena",   49);
    writeRecords[1].age = 19;  writeRecords[1].gpa = 4.8;

    std::strncpy(writeRecords[2].name, "Boiko Mykhailo", 49);
    writeRecords[2].age = 21;  writeRecords[2].gpa = 3.9;

    // --- Запис ---
    {
        std::ofstream outFile("students.bin", std::ios::binary);
        outFile.write(reinterpret_cast<const char*>(writeRecords),
                      3 * sizeof(StudentRecord));
    }

    // --- Читання одного запису за індексом ---
    std::ifstream inFile("students.bin", std::ios::binary);

    int         targetIndex = 1;
    StudentRecord record;

    // Переходимо до потрібного запису
    inFile.seekg(targetIndex * sizeof(StudentRecord));

    inFile.read(reinterpret_cast<char*>(&record), sizeof(StudentRecord));

    if (inFile.gcount() == sizeof(StudentRecord))
    {
        std::cout << "Запис [" << targetIndex << "]: "
                  << record.name
                  << " | вік: " << record.age
                  << " | GPA: " << record.gpa << "\n";
    }

    return 0;
}
./BinaryStruct
$ ./BinaryStruct
Запис [1]: Koval Olena | вік: 19 | GPA: 4.8
Execution finished with exit code 0.

Одночасне читання та запис: тонкощі std::fstream

Клас std::fstream поєднує можливості ifstream та ofstream, дозволяючи одночасно читати та записувати дані в один і той самий файл. Проте цей клас має важливу особливість, яка часто стає причиною багів у початківців.

Стандарт C++ (спадкувавши це від стандарту C) накладає жорстке обмеження: не можна безпосередньо переходити від операції читання до операції запису (і навпаки) без проміжного позиціонування.

Якщо ви щось прочитали, а потім хочете записати — ви повинні явно викликати метод позиціонування (наприклад, seekg(), seekp()) або очистити буфер за допомогою flush(). Навіть якщо ви хочете здійснити запис у ту саму позицію, де зараз перебуває покажчик, необхідно викликати seekg(0, std::ios::cur) (позиціонування на 0 байтів від поточної позиції).

Приклад: заміна голосних літер у файлі

Напишемо програму, яка відкриває текстовий файл, читає його символ за символом і замінює всі голосні літери на символ #:

ReplaceVowels.cpp
#include <fstream>
#include <iostream>
#include <cctype>

bool isVowel(char ch)
{
    ch = std::tolower(ch);
    return ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u' || ch == 'y';
}

int main()
{
    // Створюємо тестовий файл
    {
        std::ofstream out("vowels.txt");
        out << "Hello, stream files in C++!\n";
    }

    // Відкриваємо для читання та запису
    std::fstream file("vowels.txt", std::ios::in | std::ios::out);

    if (!file.is_open())
    {
        std::cerr << "Помилка відкриття файлу\n";
        return 1;
    }

    char ch;
    // Читаємо символ за символом
    while (file.get(ch))
    {
        if (isVowel(ch))
        {
            // Отримуємо поточну позицію читання (вона змістилася на +1 після get)
            std::streampos pos = file.tellg();

            // Переходимо назад до символу, який щойно прочитали
            file.seekp(pos - std::streamoff(1));

            // Записуємо заміну
            file.put('#');

            // КРИТИЧНО: повертаємося назад на ту саму позицію читання,
            // щоб синхронізувати потоки та продовжити цикл get()
            file.seekg(pos);
        }
    }

    file.close();
    std::cout << "Голосні літери успішно замінено\n";
    return 0;
}
./ReplaceVowels
$ g++ -std=c++17 ReplaceVowels.cpp -o ReplaceVowels && ./ReplaceVowels
Голосні літери успішно замінено
$ cat vowels.txt
H#ll#, str##m f#l#s #n C++!

Чому не можна записувати вказівники у файли

Запис простих даних (чисел, символів) або структур фіксованого розміру (POD - Plain Old Data) у бінарному режимі є тривіальним. Однак усе кардинально змінюється, коли структура містить вказівники (покажчики), динамічні масиви чи складні об'єкти на кшталт std::string або std::vector.

Вказівник містить не самі дані, а лише адресу в оперативній пам'яті (наприклад, 0x007ffe10), де ці дані зберігаються під час поточного запуску програми.

Якщо ви запишете вказівник у файл, ви запишете туди лише це число-адресу. При повторному запуску програми (через хвилину чи на іншому комп'ютері) оперативна пам'ять буде виділена абсолютно інакше. Адреса 0x007ffe10 або вказуватиме на сміття, або належатиме іншій програмі, що призведе до падіння програми (Segmentation Fault) при спробі її розіменування.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "Запуск 1 (Запис)" {
  node "ОЗУ (Запуск 1)" as RAM1 {
    card "Змінна text\nАдреса: 0x00A1\nЗначення: \"Hello\"" as Var1
    card "Структура\nПокажчик ptr = 0x00A1" as Struct1
  }
  database "Файл на диску" as Disk
  Struct1 --> Var1 : вказує на
  Struct1 -.-> Disk : "Запис структури\n(записує ptr = 0x00A1)"
}

package "Запуск 2 (Читання)" {
  node "ОЗУ (Запуск 2)" as RAM2 {
    card "Інші дані програми\nАдреса: 0x00A1" as Var2
    card "Структура\nПрочитаний ptr = 0x00A1" as Struct2
  }
  Disk -.-> Struct2 : "Читання структури\n(відновлює ptr = 0x00A1)"
  Struct2 --> Var2 : "!!! Вказує на чужу пам'ять\nабо неіснуючу адресу (Crash) !!!"
}
@enduml

Приклад небезпечного коду (НЕ РОБІТЬ ТАК!)

Спробуємо зберегти структуру, що містить std::string. Всередині std::string динамічно виділяє пам'ять у купі (heap) для збереження символів і містить покажчик на неї.

DangerousWrite.cpp
#include <fstream>
#include <iostream>
#include <string>

struct BadRecord
{
    int         id;
    std::string message; // Містить прихований покажчик на купу!
};

int main()
{
    BadRecord record1 = { 1, "Критично важливе повідомлення" };

    // Записуємо сирі байти структури
    std::ofstream out("bad.bin", std::ios::binary);
    out.write(reinterpret_cast<const char*>(&record1), sizeof(BadRecord));
    out.close();

    // Читаємо назад в іншому контексті
    BadRecord record2;
    std::ifstream in("bad.bin", std::ios::binary);
    in.read(reinterpret_cast<char*>(&record2), sizeof(BadRecord));
    in.close();

    // !!! Спроба виведення призведе до падіння програми (Crash) !!!
    // Тому що record2.message містить адресу пам'яті з попереднього запуску,
    // яка тепер є недійсною.
    // std::cout << record2.message << "\n";

    return 0;
}
Золоте правило серіалізації: ніколи не пишіть у файл об'єкти, що керують динамічною пам'яттю (std::string, std::vector, покажчики T*, розумні покажчики shared_ptr/unique_ptr) через сирий запис пам'яті (write()). Для таких об'єктів потрібно застосовувати серіалізацію — збереження фактичних даних, а не адрес. Наприклад, зберігати довжину рядка, а потім самі символи по черзі.

Сучасна робота з файловою системою: std::filesystem (C++17)

До виходу стандарту C++17 мова не мала стандартних засобів для роботи з каталогами, отримання списку файлів, перевірки їхнього існування чи створення папок. Програмістам доводилося використовувати Windows API на Windows або POSIX API на macOS/Linux, що робило код непереносним.

Починаючи з C++17, у мову впроваджено бібліотеку <filesystem> (простір імен std::filesystem), яка стандартизує ці операції.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "std::filesystem::path" {
  [Шлях до файлу\n/home/user/data.txt] as PathObj
}

node "Операції над шляхом" {
  PathObj --> "exists()" : перевірка існування
  PathObj --> "filename()" : отримати ім'я (data.txt)
  PathObj --> "extension()" : отримати розширення (.txt)
  PathObj --> "parent_path()" : батьківська папка (/home/user)
}

node "Операції над файловою системою" {
  [std::filesystem] --> "create_directory()" : створити папку
  [std::filesystem] --> "remove()" : видалити файл/папку
  [std::filesystem] --> "copy()" : скопіювати файл
  [std::filesystem] --> "directory_iterator()" : обхід каталогу
}
@enduml

Клас std::filesystem::path

Основою бібліотеки є клас path, який абстрагує поняття файлового шляху та автоматично підлаштовує його під правила конкретної ОС (наприклад, замінює прямі косі риски / в Unix на зворотні \ у Windows):

PathInfo.cpp
#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main()
{
    fs::path p("/Users/user/documents/report.pdf");

    std::cout << "Повний шлях:   " << p << "\n";
    std::cout << "Батьківська папка: " << p.parent_path() << "\n";
    std::cout << "Ім'я файлу:    " << p.filename() << "\n";
    std::cout << "Власне назва:  " << p.stem() << "\n";
    std::cout << "Розширення:    " << p.extension() << "\n";

    if (p.is_absolute())
    {
        std::cout << "Шлях є абсолютним\n";
    }

    return 0;
}
./PathInfo
$ g++ -std=c++17 PathInfo.cpp -o PathInfo && ./PathInfo
Повний шлях: "/Users/user/documents/report.pdf"
Батьківська папка: "/Users/user/documents"
Ім'я файлу: "report.pdf"
Власне назва: "report"
Розширення: ".pdf"
Шлях є абсолютним

Отримання списку файлів у каталозі

Для обходу вмісту папки використовується directory_iterator:

ListDirectory.cpp
#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main()
{
    fs::path targetDir = "."; // Поточна директорія

    std::cout << "Список файлів у папці " << fs::absolute(targetDir) << ":\n\n";

    try
    {
        // directory_iterator обходить елементи папки (без рекурсії)
        // Для рекурсивного обходу є recursive_directory_iterator
        for (const auto& entry : fs::directory_iterator(targetDir))
        {
            const auto& path = entry.path();
            
            if (fs::is_directory(path))
            {
                std::cout << "[DIR]  " << path.filename().string() << "\n";
            }
            else if (fs::is_regular_file(path))
            {
                std::cout << "[FILE] " << path.filename().string() 
                          << " (" << fs::file_size(path) << " байтів)\n";
            }
        }
    }
    catch (const fs::filesystem_error& e)
    {
        std::cerr << "Помилка файлової системи: " << e.what() << "\n";
    }

    return 0;
}
./ListDirectory
$ g++ -std=c++17 ListDirectory.cpp -o ListDirectory && ./ListDirectory
Список файлів у папці /Users/arakviel/Work/kostyl.dev:
[FILE] package.json (558 байтів)
[DIR] content
[DIR] public
[FILE] app.config.ts (556 байтів)
[FILE] nuxt.config.ts (2534 байтів)

Корисні маніпуляції з файлами та папками

Бібліотека надає прості, переносні функції для типових завдань ОС:

FolderOps.cpp
#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main()
{
    fs::path newDir = "test_sandbox";
    fs::path file1  = newDir / "note.txt"; // Оператор / з'єднує шляхи
    fs::path file2  = newDir / "backup.txt";

    // 1. Створення папки
    if (fs::create_directory(newDir))
    {
        std::cout << "Створено папку: " << newDir << "\n";
    }

    // Створимо файл для тестування
    std::ofstream(file1) << "Hello, filesystem!\n";

    // 2. Перевірка існування
    if (fs::exists(file1))
    {
        std::cout << "Файл " << file1.filename() << " існує\n";
    }

    // 3. Копіювання файлу
    fs::copy_file(file1, file2, fs::copy_options::overwrite_existing);
    std::cout << "Файл скопійовано\n";

    // 4. Перейменування/переміщення
    fs::path movedFile = newDir / "renamed_note.txt";
    fs::rename(file1, movedFile);
    std::cout << "Файл перейменовано\n";

    // 5. Видалення файлу або папки
    fs::remove(file2);      // видаляє один файл
    fs::remove_all(newDir); // рекурсивно видаляє папку з усім вмістом
    std::cout << "Очищення завершено, пісочницю видалено\n";

    return 0;
}
./FolderOps
$ g++ -std=c++17 FolderOps.cpp -o FolderOps && ./FolderOps
Створено папку: "test_sandbox"
Файл "note.txt" існує
Файл скопійовано
Файл перейменовано
Очищення завершено, пісочницю видалено
Execution finished with exit code 0.

Порівняльна таблиця: C-стиль vs C++-стиль

ОпераціяC-стиль (cstdio / stdio.h)C++-стиль (fstream)
Опис потокуFILE* streamstd::ifstream, std::ofstream, std::fstream
Відкриттяstream = fopen(name, mode)stream.open(name, mode) або через конструктор
Перевірка успіхуif (stream == NULL)if (!stream) або if (!stream.is_open())
Закриттяfclose(stream)stream.close() або автоматично (RAII)
Текстовий вивідfprintf(stream, "%d %s", val, str)stream << val << " " << str
Текстовий ввідfscanf(stream, "%d %s", &val, buf)stream >> val >> str
Рядковий вивідfputs(str, stream)stream << str
Рядковий ввідfgets(buf, size, stream)std::getline(stream, str)
Посимвольний вивідfputc(ch, stream)stream.put(ch)
Посимвольний ввідch = fgetc(stream)stream.get(ch)
Бінарний вивідfwrite(ptr, size, count, stream)stream.write(reinterpret_cast<const char*>(ptr), size)
Бінарний ввідfread(ptr, size, count, stream)stream.read(reinterpret_cast<char*>(ptr), size)
Позиціонуванняfseek(stream, offset, origin)stream.seekg(offset, dir) / stream.seekp(...)
Отримання позиціїftell(stream)stream.tellg() / stream.tellp()
Скидання буфераfflush(stream)stream.flush() або маніпулятор std::flush
Перевірка EOFfeof(stream)stream.eof()
Прапорець помилкиferror(stream)stream.fail() або stream.bad()
Скидання помилокclearerr(stream)stream.clear()
Видалення файлуremove(filename)std::filesystem::remove(path)
Перейменуванняrename(oldname, newname)std::filesystem::rename(oldpath, newpath)

Схема життєвого циклу файлового потоку в C++

Loading diagram...
flowchart TD
    A["Створити об'єкт потоку\nifstream / ofstream / fstream"] --> B["Відкрити файл\nчерез конструктор або open()"]
    B --> C{Файл відкрито?\nis_open() або !fail()}
    C -- Ні --> D["Викликати exceptions() або\nобробити помилку вручну"]
    D --> Z["Завершення / Аварійний вихід"]
    C -- Так --> E["Виконати операції\n<<, >>, read(), write()"]
    E --> F{Помилка чи EOF?\nfail() / eof() / bad()}
    F -- Так --> G{Потрібно продовжити?}
    G -- Так --> H["clear() - скинути стан\nseekg() / seekp() - перемістити покажчик"]
    H --> E
    G -- Ні --> I["close() або\nвихід з області видимості"]
    F -- Ні --> I
    I --> J["Ресурси звільнено\nбуфер скинуто (RAII)"]

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style C fill:#f59e0b,stroke:#b45309,color:#ffffff
    style D fill:#ef4444,stroke:#dc2626,color:#ffffff
    style E fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style F fill:#f59e0b,stroke:#b45309,color:#ffffff
    style G fill:#f59e0b,stroke:#b45309,color:#ffffff
    style H fill:#64748b,stroke:#334155,color:#ffffff
    style I fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style J fill:#22c55e,stroke:#16a34a,color:#ffffff
    style Z fill:#ef4444,stroke:#dc2626,color:#ffffff

Практика

Рівень 1 — Базові операції з текстовими файлами

Рівень 2 — Бінарні файли та серіалізація

Рівень 3 — Робота з std::filesystem


Резюме

🤖 Об'єктно-орієнтований ввід/вивід

Класи ifstream (читання), ofstream (запис) та fstream (читання + запис) інтегруються в загальну систему потоків C++. Робота з файлами будується на тих самих принципах, що й з std::cin і std::cout, забезпечуючи повну типобезпеку та інтуїтивно зрозумілий синтаксис.

🛡️ Безпека ресурсів (RAII)

Потоки C++ автоматично закривають файли та очищають буфери у своїх деструкторах під час виходу об'єктів з області видимості. Це запобігає витоку системних дескрипторів та втраті накопичених даних навіть у випадку виникнення виключень.

📈 Буферизація та маніпулятори

Вивід буферизується для оптимізації швидкості роботи з диском. Маніпулятор std::endl не тільки додає символ нового рядка \n, але й примусово очищає буфер (flush), що може призводити до падіння продуктивності у великих циклах. Використовуйте \n для оптимізації.

🎯 Контроль стану потоку

Стан потоку контролюється прапорцями помилок (good(), fail(), eof(), bad()). За потреби можна налаштувати потоки на генерацію виключень. Функція clear() скидає прапорці помилок, що дозволяє відновити роботу з потоком після некоректного вводу.

📂 Сучасне API файлової системи

Бібліотека std::filesystem з C++17 надає потужні, переносні та об'єктно-орієнтовані засоби для навігації каталогами, перевірки атрибутів, копіювання, перейменування та видалення файлів і папок на рівні операційної системи.
Файлові потоки C++ разом із бібліотекою std::filesystem формують сучасний, безпечний і зручний інструментарій розробника. Вони абстрагують фізичну організацію зберігання даних, дозволяючи зосередитися на бізнес-логіці програми. У наступних розділах ми розглянемо, як поєднувати потоки із контейнерами стандартної бібліотеки (STL) для створення гнучких і швидкодійних систем збереження інформації.
Copyright © 2026