Робота файлового вводу/виводу в мові C++ майже аналогічна роботі звичайних потоків вводу/виводу (але зі своїми нюансами).
Класи файлового вводу/виводу
Є три основні класи файлового вводу/виводу в мові C++:
ifstream (є дочірнім класу istream);
ofstream (є дочірнім класу ostream);
fstream (є дочірнім класу iostream).
За допомогою цих класів можна виконувати однонаправлений файловий ввід, однонаправлений файловий вивід і двонаправлений файловий ввід/вивід. Для їх використання потрібно всього лише підключити заголовок fstream.
На відміну від потоків cout, cin, cerr і clog, які відразу ж можна використовувати, файлові потоки повинні бути явно встановлені програмістом. Тобто, щоб відкрити файл для читання і/або запису, потрібно створити об’єкт відповідного класу файлового вводу/виводу, вказавши ім’я файлу в якості параметра. Потім, за допомогою оператора вставки (<<) або оператора вилучення (>>), можна записувати дані в файл або зчитувати вміст файлу. Після виконання даних дій потрібно закрити файл — явно викликати метод close() або просто дозволити файловій змінній вводу/виводу вийти з області видимості (деструктор файлового класу вводу/виводу закриє цей файл автоматично замість нас).
Файловий вивід
Для запису в файл використовується клас ofstream. Наприклад:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <iostream> #include <fstream> #include <cstdlib> // для використання функції exit() int main() { using namespace std; // Клас ofstream використовується для запису даних в файл. // Створюємо файл SomeText.txt ofstream outf("SomeText.txt"); // Якщо ми не можемо відкрити цей файл для запису даних, if (!outf) { // то виводимо повідомлення про помилку і виконуємо функцію exit() cerr << "Uh oh, SomeText.txt could not be opened for writing!" << endl; exit(1); } // Записуємо в файл наступні два рядки outf << "See line #1!" << endl; outf << "See line #2!" << endl; return 0; // Коли outf вийде з області видимості, то деструктор класу ofstream автоматично закриє наш файл } |
Якщо ви заглянете в каталог вашого проекту (ПКМ по вкладці з назвою вашого .cpp-файлу в Visual Studio > “Открыть содержащую папку”), то побачите файл з ім’ям SomeText.txt, в якому знаходяться наступні рядки:
See line #1!
See line #2!
Зверніть увагу, ми також можемо використати метод put() для запису одного символу в файл.
Файловий ввід
Тепер ми спробуємо прочитати вміст файлу, який створили в попередньому прикладі. Зверніть увагу, ifstream поверне 0, якщо ми досягли кінця файлу (це зручно для визначення «довжини» вмісту файлу). Наприклад:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <iostream> #include <fstream> #include <string> #include <cstdlib> // для використання функції exit() int main() { using namespace std; // ifstream використовується для читання вмісту файла. // Спробуємо прочитати вміст файлу SomeText.txt ifstream inf("SomeText.txt"); // Якщо ми не можемо відкрити цей файл для читання його вмісту, if (!inf) { // то виводимо наступне повідомлення про помилку і виконуємо функцію exit() cerr << "Uh oh, SomeText.txt could not be opened for reading!" << endl; exit(1); } // Поки є дані, які ми можемо прочитати, while (inf) { // то переміщуємо ці дані в рядок, який потім виводимо на екран string strInput; inf >> strInput; cout << strInput << endl; } return 0; // Коли inf вийде з області видимості, то деструктор класу ifstream автоматично закриє наш файл } |
Результат виконання програми:
See
line
#1!
See
line
#2!
Хм, це не зовсім те, що ми хотіли. Як ми вже дізналися на попередніх уроках, оператор вилучення працює з «відформатованими даними», тобто він ігнорує всі пробіли, символи табуляції і символ нового рядка. Щоб прочитати весь вміст як є, без його ділення на частини (як у вищенаведеному прикладі), нам потрібно використати метод getline():
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <iostream> #include <fstream> #include <string> #include <cstdlib> // для використання функції exit() int main() { using namespace std; // ifstream використовується для читання вмісту файлів. // Ми спробуємо прочитати вміст файлу SomeText.txt ifstream inf("SomeText.txt"); // Якщо ми не можемо відкрити файл для читання його вмісту, if (!inf) { // то виводимо наступне повідомлення про помилку і виконуємо функцію exit() cerr << "Uh oh, SomeText.txt could not be opened for reading!" << endl; exit(1); } // Поки є, що читати, while (inf) { // то переміщуємо те, що можемо прочитати, в рядок, а потім виводимо цей рядок на екран string strInput; getline(inf, strInput); cout << strInput << endl; } return 0; // Коли inf вийде з області видимості, то деструктор класу ifstream автоматично закриє наш файл } |
Результат виконання програми:
Буферизований вивід
Вивід в мові C++ може бути буферизований. Це означає, що все, що виводиться в файловий потік, не може відразу ж бути записано на диск (в конкретний файл). Це зроблено, в першу чергу, з міркувань продуктивності. Коли дані буфера записуються на диск, то це називається очищенням буфера. Одним із способів очищення буфера є закриття файлу. У такому випадку весь вміст буфера буде переміщено на диск, а потім файл буде закрито.
Буферизація виводу зазвичай не є проблемою, але при певних обставинах вона може викликати проблеми у необережних новачків. Наприклад, коли в буфері зберігаються дані, а програма передчасно завершує своє виконання (або в результаті збою, або шляхом виклику функції exit()). У таких випадках деструктори класів файлового вводу/виводу не виконуються, файли ніколи не закриваються, буфери не очищаються і наші дані губляться назавжди. Ось чому гарною ідеєю є явне закриття всіх відкритих файлів перед викликом функції exit().
Також буфер можна очистити вручну, використовуючи метод ostream::flush() або відправивши std::flush в вихідний потік. Будь-який з цих способів може бути корисний для забезпечення негайного запису вмісту буфера на диск в разі збою програми.
Цікавий нюанс: Оскільки std::endl; також очищає вихідний потік, то його надмірне використання (яке призводить до непотрібних очищень буфера) може вплинути на продуктивність програми (тому що очищення буфера в деяких випадках може бути витратною операцією). З цієї причини програмісти, які турбуються про продуктивність свого коду, часто використовують \n замість std::endl для вставки символу нового рядка у вихідний потік, щоб уникнути непотрібного очищення буфера.
Режими відкриття файлів
Що відбудеться, якщо ми спробуємо записати дані в уже існуючий файл? Повторний запуск вищенаведеної програми (найперша) показує, що вихідний файл повністю перезаписується при повторному запуску програми. А що, якщо нам потрібно додати дані в кінець файлу? Виявляється, конструктори файлового потоку приймають необов’язковий другий параметр, який дозволяє вказати програмісту спосіб відкриття файлу. В якості цього параметра можна передавати наступні флаги (які знаходяться в класі ios):
app — відкриває файл в режимі додавання;
ate — переходить в кінець файлу перед читанням/записом;
binary — відкриває файл в бінарному режимі (замість текстового режиму);
in — відкриває файл в режимі читання (за замовчуванням для ifstream);
out — відкриває файл в режимі запису (за замовчуванням для ofstream);
trunc — видаляє файл, якщо він вже існує.
Можна вказати відразу кілька флагів шляхом використання побітового АБО (|).
ifstream за замовчуванням працює в режимі ios::in;
ofstream за замовчуванням працює в режимі ios::out;
fstream за замовчуванням працює в режимі ios::in АБО ios::out, що означає, що ви можете виконувати як читання вмісту файлу, так і запис даних в файл.
Тепер давайте напишемо програму, яка додасть два рядки в раніше створений нами файл SomeText.txt:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <iostream> #include <cstdlib> // для використання функції exit() #include <fstream> int main() { using namespace std; // Передаємо флаг ios:app, щоб повідомити fstream, що ми збираємося додати свої дані до вже існуючих даних файлу. // Ми не збираємося перезаписувати файл. // Нам не потрібно передавати флаг ios::out, оскільки ofstream за замовчуванням працює в режимі ios::out ofstream outf("SomeText.txt", ios::app); // Якщо ми не можемо відкрити файл для запису даних, if (!outf) { // то виводимо наступне повідомлення про помилку і виконуємо функцію exit() cerr << "Uh oh, SomeText.txt could not be opened for writing!" << endl; exit(1); } outf << "See line #3!" << endl; outf << "See line #4!" << endl; return 0; // Коли outf вийде з області видимості, то деструктор класу ofstream автоматично закриє наш файл } |
Тепер, якщо ми подивимося на вміст файлу SomeText.txt (запустимо одну з вищенаведених програм для читання файлу або відкриємо цей файл в каталозі проекту), то побачимо наступне:
See line #1!
See line #2!
See line #3!
See line #4!
Явне відкриття файлів за допомогою функції open()
Точно так же, як ми явно закриваємо файл за допомогою методу close(), ми можемо явно відкривати файл за допомогою функції open(). Функція open() працює аналогічно конструкторам класу файлового вводу/виводу: приймає ім’я файлу і режим (необов’язково), в якому потрібно відкрити файл, в якості параметрів. Наприклад:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <fstream> int main() { using namespace std; ofstream outf("SomeText.txt"); outf << "See line #1!" << endl; outf << "See line #2!" << endl; outf.close(); // явно закриваємо файл // Упс, ми дещо забули зробити outf.open("SomeText.txt", ios::app); outf << "See line #3!" << endl; outf.close(); return 0; // Коли outf вийде з області видимості, то деструктор класу ofstream автоматично закриє наш файл } |
Результат:
See line #1!
See line #2!
See line #3!
На цьому все! На наступному уроці ми розглянемо рандомний файловий ввід/вивід.
