Робота з файлами: C++-стиль (fstream)
Робота з файлами: C++-стиль (fstream)
Від stdio.h до fstream: чому з'явився новий підхід
У попередньому розділі ми детально вивчили класичний C-підхід до роботи з файлами через бібліотеку <cstdio>. Він ефективний, переносний і повсюдно підтримуваний — але є суттєвим анахронізмом у контексті сучасного C++.
Найголовніший його недолік: відсутність типобезпеки. Функція fscanf(file, "%d", &value) приймає довільний вказівник і ніяк не гарантує, що тип змінної відповідає специфікатору формату. Помилка на кшталт fscanf(file, "%f", &intVar) — тихий UB (невизначена поведінка), яку компілятор не зловить.
C++ пропонує принципово інший підхід, побудований на тих самих концепціях потоків (streams), що й для консольного вводу/виводу. Ви вже знайомі з std::cin і std::cout — ifstream, ofstream і fstream є їхніми файловими аналогами, з тими самими операторами та методами.
Переваги C++-підходу
- Типобезпека: оператори
<<і>>завжди знають тип даних під час компіляції - RAII: деструктор автоматично закриває файл при виході зі scope
- Єдиний синтаксис: той самий
<</>>, що і зcout/cin - Обробка помилок: стан потоку через
good(),fail(),eof(),bad()
Коли ще використовують C-стиль
- Бінарні файли з точним контролем байтів (
fread/fwrite) - Взаємодія з legacy-кодом або C-бібліотеками
- Максимальна продуктивність (нижчий overhead)
- Платформно-специфічний код (POSIX API)
Ієрархія класів файлового вводу/виводу
Бібліотека <fstream> побудована на вже знайомій вам ієрархії потокових класів:

Ця ієрархія важлива: оскільки ifstream успадковує istream, він підтримує всі методи та оператори, доступні для std::cin. Аналогічно ofstream підтримує все, що є у std::cout. Це робить перехід між файловим і консольним вводом/виводом абсолютно прозорим.
Для підключення бібліотеки:
#include <fstream> // ifstream, ofstream, fstream
// Додатково може знадобитися:
#include <string> // std::string, std::getline
#include <sstream> // std::istringstream, std::ostringstream
<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 підтримує оператор вставки << для запису будь-яких типів даних:
#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;
}
Явне закриття: метод close()
Попри RAII, іноді потрібно закрити файл явно — наприклад, щоб одразу після закриття відкрити його для читання:
#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:
#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;
}
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 або помилці).
#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;
}
Режими відкриття файлів
На відміну від C-стилю (де режим задається рядком: "r", "wb" і т.д.), у C++ режим задається через константи класу std::ios. Вони можуть комбінуватися побітовим АБО (|).
| Прапорець | Значення |
|---|---|
ios::in | Відкрити для читання (за замовчуванням для ifstream) |
ios::out | Відкрити для запису (за замовчуванням для ofstream) |
ios::app | Дозапис у кінець файлу |
ios::ate | Перейти в кінець після відкриття |
ios::trunc | Очистити файл при відкритті (за замовчуванням для ios::out) |
ios::binary | Відкрити в бінарному режимі |
Режим дозапису: ios::app
#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;
}
Режим оновлення: ios::in | ios::out
fstream з комбінацією ios::in | ios::out відкриває існуючий файл для одночасного читання і запису, не очищаючи його:
#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(). Це корисно, коли ім'я файлу невідоме до виконання, або коли потрібно перевикористати об'єкт потоку:
#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), що відображають його поточний стан:
Перевірка і скидання стану
#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;
}
Виключення з потоків
За бажанням можна налаштувати потік так, щоб він кидав виключення замість встановлення прапорців. Це корисно у коді, де виключення вже є основним механізмом обробки помилок:
#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;
}
failbit (якщо читання не вдалося через кінець файлу) також може бути кинуто виключення. Тому налаштування exceptions() рекомендується лише для відкриття файлів, а не для операцій читання в циклі.Робота зі структурами: читання і запис
Розглянемо повноцінний приклад: запис і читання масиву структур-студентів у текстовий файл.
#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;
}
<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' для рядків, що не вимагають негайного запису.Приклад: лог-файл із гарантованим записом
#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.

Визначення розміру файлу
#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;
}
Читання всього файлу в рядок
Комбінація seekg(0, ios::end) + tellg() + seekg(0, ios::beg) — класичний прийом завантаження всього файлу в std::string одним рядком:
#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;
}
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. Це нормально для бінарного вводу/виводу — ми свідомо інтерпретуємо пам'ять як послідовність байтів.Запис і читання масиву чисел
#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;
}
Запис структур у бінарний файл
Найпотужніша особливість бінарного вводу/виводу — можливість зберегти структуру одним викликом:
#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;
}
Одночасне читання та запис: тонкощі std::fstream
Клас std::fstream поєднує можливості ifstream та ofstream, дозволяючи одночасно читати та записувати дані в один і той самий файл. Проте цей клас має важливу особливість, яка часто стає причиною багів у початківців.
Стандарт C++ (спадкувавши це від стандарту C) накладає жорстке обмеження: не можна безпосередньо переходити від операції читання до операції запису (і навпаки) без проміжного позиціонування.
Якщо ви щось прочитали, а потім хочете записати — ви повинні явно викликати метод позиціонування (наприклад, seekg(), seekp()) або очистити буфер за допомогою flush(). Навіть якщо ви хочете здійснити запис у ту саму позицію, де зараз перебуває покажчик, необхідно викликати seekg(0, std::ios::cur) (позиціонування на 0 байтів від поточної позиції).
Приклад: заміна голосних літер у файлі
Напишемо програму, яка відкриває текстовий файл, читає його символ за символом і замінює всі голосні літери на символ #:
#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;
}
Чому не можна записувати вказівники у файли
Запис простих даних (чисел, символів) або структур фіксованого розміру (POD - Plain Old Data) у бінарному режимі є тривіальним. Однак усе кардинально змінюється, коли структура містить вказівники (покажчики), динамічні масиви чи складні об'єкти на кшталт std::string або std::vector.
Вказівник містить не самі дані, а лише адресу в оперативній пам'яті (наприклад, 0x007ffe10), де ці дані зберігаються під час поточного запуску програми.
Якщо ви запишете вказівник у файл, ви запишете туди лише це число-адресу. При повторному запуску програми (через хвилину чи на іншому комп'ютері) оперативна пам'ять буде виділена абсолютно інакше. Адреса 0x007ffe10 або вказуватиме на сміття, або належатиме іншій програмі, що призведе до падіння програми (Segmentation Fault) при спробі її розіменування.

Приклад небезпечного коду (НЕ РОБІТЬ ТАК!)
Спробуємо зберегти структуру, що містить std::string. Всередині std::string динамічно виділяє пам'ять у купі (heap) для збереження символів і містить покажчик на неї.
#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), яка стандартизує ці операції.

Клас std::filesystem::path
Основою бібліотеки є клас path, який абстрагує поняття файлового шляху та автоматично підлаштовує його під правила конкретної ОС (наприклад, замінює прямі косі риски / в Unix на зворотні \ у Windows):
#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;
}
Отримання списку файлів у каталозі
Для обходу вмісту папки використовується directory_iterator:
#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;
}
Корисні маніпуляції з файлами та папками
Бібліотека надає прості, переносні функції для типових завдань ОС:
#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;
}
Порівняльна таблиця: C-стиль vs C++-стиль
| Операція | C-стиль (cstdio / stdio.h) | C++-стиль (fstream) |
|---|---|---|
| Опис потоку | FILE* stream | std::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 |
| Перевірка EOF | feof(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++
Практика
Рівень 1 — Базові операції з текстовими файлами
Напишіть програму, яка:
- Запитує у користувача шлях до файлу.
- Відкриває його за допомогою
std::ifstream. Якщо файлу немає, повідомляє про помилку і запитує знову. - Читає з файлу послідовність цілих чисел за допомогою оператора
>>. - Якщо в процесі читання виявлено нечислові дані (наприклад, слова), програма повинна повідомити про це, скинути помилку через
clear(), прочитати невірне слово в рядок-сміття і продовжити читання решти чисел. - Виводить суму та кількість правильно зчитаних чисел.
main.cpp) посимвольно (get()) і перевіряє баланс круглих (), фігурних {} та квадратних [] дужок. Програма має виводити повідомлення про те, чи коректно закриті всі дужки, або вказувати на перший символ порушення балансу.Рівень 2 — Бінарні файли та серіалізація
Оскільки std::string не можна писати у файл як сиру структуру, реалізуйте ручну серіалізацію.
Створіть структуру Product:
std::string title— назва товару (довільної довжини)int count— кількість на складіdouble price— ціна
Напишіть дві функції:
void serializeProduct(std::ostream& os, const Product& p): спочатку записує розмір рядкаtitle(типsize_t), далі — сирі символи рядка черезwrite(), далі — змінніcountтаprice.Product deserializeProduct(std::istream& is): читає розмір, виділяє буфер під рядок відповідної довжини, зчитує символи рядка, зчитуєcountтаprice.
Програма має створювати файл inventory.bin, записувати туди список товарів, а потім зчитувати та виводити їх на екран.
Створіть бінарний файл grades.dat, який зберігає оцінки у вигляді послідовності int (кожен оцінка — один int). Напишіть програму, яка:
- Отримує від користувача індекс оцінки, яку треба змінити.
- За допомогою
seekp()переходить до потрібної оцінки. - Записує нове значення.
- За допомогою
seekg()зчитує та виводить оновлений масив оцінок.
Рівень 3 — Робота з std::filesystem
Напишіть програму, яка автоматизує резервне копіювання:
- Отримує шлях до цільової папки від користувача.
- Перевіряє її існування через
std::filesystem::exists(). - Створює нову папку
backupу поточній директорії. - Обходить усі файли у цільовій папці за допомогою
directory_iterator. - Копіює всі файли з розширенням
.txtабо.docxу папкуbackupза допомогоюstd::filesystem::copy_file(). - Виводить список скопійованих файлів та їхній сумарний розмір.
Резюме
🤖 Об'єктно-орієнтований ввід/вивід
ifstream (читання), ofstream (запис) та fstream (читання + запис) інтегруються в загальну систему потоків C++. Робота з файлами будується на тих самих принципах, що й з std::cin і std::cout, забезпечуючи повну типобезпеку та інтуїтивно зрозумілий синтаксис.🛡️ Безпека ресурсів (RAII)
📈 Буферизація та маніпулятори
std::endl не тільки додає символ нового рядка \n, але й примусово очищає буфер (flush), що може призводити до падіння продуктивності у великих циклах. Використовуйте \n для оптимізації.🎯 Контроль стану потоку
good(), fail(), eof(), bad()). За потреби можна налаштувати потоки на генерацію виключень. Функція clear() скидає прапорці помилок, що дозволяє відновити роботу з потоком після некоректного вводу.📂 Сучасне API файлової системи
std::filesystem з C++17 надає потужні, переносні та об'єктно-орієнтовані засоби для навігації каталогами, перевірки атрибутів, копіювання, перейменування та видалення файлів і папок на рівні операційної системи.std::filesystem формують сучасний, безпечний і зручний інструментарій розробника. Вони абстрагують фізичну організацію зберігання даних, дозволяючи зосередитися на бізнес-логіці програми. У наступних розділах ми розглянемо, як поєднувати потоки із контейнерами стандартної бібліотеки (STL) для створення гнучких і швидкодійних систем збереження інформації.Робота з файлами: C-стиль (stdio.h)
Класичний підхід до роботи з файлами в C/C++ через бібліотеку stdio.h. Структура FILE, функції fopen/fclose, текстові та бінарні файли, форматований і посимвольний ввід/вивід, прямий доступ та функції позиціонування.
План навчання: Курс C++ — Продовження (Статті 29–60+)