На цьому уроці ми розглянемо реалізацію рандомного файлового вводу/виводу в мові С++.
Файловий вказівник
Кожен клас файлового вводу/виводу містить файловий вказівник, який використовується для відстеження поточної позиції читання/запису даних в файлі. Будь-який запис в файл або читання вмісту файлу відбувається в поточному розташуванні файлового вказівника. За замовчуванням, при відкритті файлу для читання або запису, файловий вказівник знаходиться на самому початку цього файлу. Однак, якщо файл відкривається в режимі додавання, то файловий вказівник переміщається в кінець файлу, щоб користувач мав можливість додати дані в файл, а не перезаписати його.
Рандомний доступ до файлів за допомогою функцій seekg() і seekp()
До цього моменту ми здійснювали послідовний доступ до файлів, тобто виконували читання/запис файлу по порядку. Проте, ми можемо виконати і довільний (рандомний) доступ до файлу (тобто переміщатися по файлу, як захочемо). Це може бути корисно, коли файл має великий вміст, а нам потрібен всього лише невеликий конкретний запис з цього всього. Замість послідовного доступу (коли ми переходимо до потрібного запису починаючи з самого початку файлу), ми можемо здійснити безпосередній доступ до цього запису.
Рандомний доступ до файлу здійснюється шляхом маніпулювання файловим вказівником за допомогою функції seekg() (закінчення “g” = “get”, тобто “отримати/дістати”) — для вводу, і функції seekp() (закінчення “p” = “put” (тобто “покласти/помістити”) — для виводу.
Функції seekg() і seekp() приймають наступні два параметри:
перший параметр — це зміщення на яке слід перемістити файловий вказівник (вимірюється в байтах);
другий параметр — це флаг ios, який позначає місце, від якого слід відштовхуватися при виконанні зміщення.
Флаги ios, які приймають функції seekg() і seekp() в якості другого параметра:
beg — зміщення відносно початку файлу (за замовчуванням);
cur — зміщення відносно поточного розташування файлового вказівника;
end — зміщення відносно кінця файлу.
Додатне зміщення означає переміщення файлового вказівника в бік кінця файлу, тоді як від’ємне зміщення означає переміщення файлового вказівника в бік початку файлу. Наприклад:
1 2 3 4 5 |
inf.seekg(15, ios::cur); // переміщаємося вперед на 15 байтів відносно поточного розташування файлового вказівника inf.seekg(-17, ios::cur); // переміщаємося назад на 17 байтів відносно поточного розташування файлового вказівника inf.seekg(24, ios::beg); // переміщаємося до 24-го байту відносно початку файлу inf.seekg(25); // переміщаємося до 25-го байту файлу inf.seekg(-27, ios::end); // переміщаємося до 27-го байту від кінця файлу |
Переміщення в початок або в кінець файлу:
1 2 |
inf.seekg(0, ios::beg); // переміщаємося в початок файлу inf.seekg(0, ios::end); // переміщаємося в кінець файлу |
Тепер давайте поєднаємо функцію seekg() з файлом SomeText.txt (який ми використовували на попередньому уроці).
Вміст файлу SomeText.txt:
See line #1!
See line #2!
See line #3!
See line #4!
Код програми:
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 35 36 37 38 |
#include <iostream> #include <fstream> #include <string> #include <cstdlib> // для використання функції exit() int main() { using namespace std; ifstream inf("SomeText.txt"); // Якщо ми не можемо відкрити файл для читання його вмісту, if (!inf) { // то виводимо повідомлення про помилку і виконуємо функцію exit() cerr << "Uh oh, SomeText.txt could not be opened for reading!" << endl; exit(1); } string strData; inf.seekg(6); // переміщаємося до 6-го символу першого рядка // Отримуємо решту рядка і виводимо на екран getline(inf, strData); cout << strData << endl; inf.seekg(9, ios::cur); // переміщаємося вперед на 9 байтів відносно поточного розташування файлового вказівника // Отримуємо решту рядка і виводимо на екран getline(inf, strData); cout << strData << endl; inf.seekg(-14, ios::end); // переміщаємося на 14 байтів назад відносно кінця файлу // Отримуємо решту рядка і виводимо на екран getline(inf, strData); cout << strData << endl; return 0; } |
Результат виконання програми:
ne #1!
#2!
See line #4!
Примітка: У деяких компіляторах реалізація функцій seekg() і tellg() при використанні з текстовими файлами може мати баги (через буферизацію даних). Якщо ваш компілятор є одним з таких (ваш результат відрізнятиметься від вищенаведеного результату), то ви можете спробувати відкрити файл в бінарному режимі:
1 |
ifstream inf("SomeText.txt", ifstream::binary); |
Є ще дві інші корисні функції — tellg() і tellp(), які повертають абсолютну позицію файлового вказівника. Це корисно при визначенні розміру файлу:
1 2 3 4 5 6 7 8 9 |
#include <iostream> #include <fstream> int main() { std::ifstream inf("SomeText.txt"); inf.seekg(0, std::ios::end); // переміщаємося в кінець файлу std::cout << inf.tellg(); } |
Результат:
56
Це ми отримали розмір файлу SomeText.txt в байтах.
Одночасне читання і запис в файл за допомогою fstream
Клас fstream
(майже) здатний одночасно читати вміст файлу і записувати дані в нього! Нюанс полягає в тому, що ви не можете довільно переключатися між читанням і записом файлу. Як тільки почнеться читання або запис файлу, то єдиним способом переключитися між читанням або записом буде виконання операції, яка змінить поточний стан файлового вказівника (наприклад, пошук даних). Якщо ви не хочете переміщати файловий вказівник (бо він вже знаходиться в потрібному місці), то ви можете просто виконати пошук поточних даних (на які вказує файловий вказівник):
1 2 |
// Припустимо, що iofile є об'єктом класу fstream iofile.seekg(iofile.tellg(), ios::beg); // переміщаємося до поточної позиції файлового вказівника |
Тепер давайте напишемо програму, яка відкриє файл, прочитає його вміст і замінить всі знайдені голосні літери на символ #
:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
#include <iostream> #include <fstream> #include <cstdlib> // для використання функції exit() int main() { using namespace std; // Ми повинні вказати як in, так і out, оскільки використовуємо fstream fstream iofile("SomeText.txt", ios::in | ios::out); // Якщо ми не можемо відкрити iofile, if (!iofile) { // то виводимо повідомлення про помилку і виконуємо функцію exit() cerr << "Uh oh, SomeText.txt could not be opened!" << endl; exit(1); } char chChar; // Поки є дані для обробки while (iofile.get(chChar)) { switch (chChar) { // Якщо ми знайшли голосну букву, case 'a': case 'e': case 'i': case 'o': case 'u': case 'A': case 'E': case 'I': case 'O': case 'U': // то переміщаємося на один символ назад відносно поточного розташування файлового вказівника iofile.seekg(-1, ios::cur); // Оскільки ми виконали операцію пошуку, то тепер можемо переключитися на запис даних в файл. // Замінимо знайдену голосну букву символом # iofile << '#'; // Тепер нам потрібно повернутися назад в режим читання файлу. // Виконуємо функцію seekg() до поточної позиції iofile.seekg(iofile.tellg(), ios::beg); break; } } return 0; } |
Результат виконання програми (вміст файлу SomeText.txt):
S## l#n# #1!
S## l#n# #2!
S## l#n# #3!
S## l#n# #4!
Інші корисні методи класів файлового вводу/виводу в мові C++:
remove() — видаляє файл;
is_open() — повертає true
, якщо потік в даний момент відкритий, і false
— якщо закритий.
Попередження про запис вказівників в файли
Хоча записувати змінні в файл досить просто, все стає складнішим, коли ми починаємо працювати з вказівниками. Як ми вже знаємо, вказівник містить лише адресу змінної, на яку він вказує. Хоча ці адреси можна записувати в файл і зчитувати їх з файлу — це може спричиняти проблеми, тому що адреса однієї і тієї ж змінної може відрізнятися при кожному повторному запуску програми. Відповідно, хоча змінна могла перебувати за адресою 003AFCD4
, коли ви записували цю адресу на диск (в будь-який файл), при повторному запуску програми вона вже може знаходитися за іншою адресою!
Наприклад, припустимо, що у нас є змінна someValue
типу int, яка знаходиться за адресою 003AFCD4
. Ми присвоюємо someValue
значення 7
. Потім оголошуємо вказівник *pnValue
, який вказує на someValue
(адресою someValue
є 003AFCD4
). Ми записуємо значення 7
і значення pnValue
(003AFCD4
) в будь-який файл.
Через кілька тижнів ми знову запускаємо цю програму і намагаємося вилучити значення з файлу. Ми вилучаємо значення 7
в змінну someValue
, яка в поточній програмі вже знаходиться за адресою 0034FD90
. Далі ми вилучаємо адресу 003AFCD4
у вказівник *pnValue
. Оскільки pnValue
вказує на 003AFCD4
, а someValue
знаходиться за адресою 0034FD90
, то pnValue
більше не вказує на someValue
, і спроба доступу до значення адреси, яку зберігає pnValue
, призведе до неприємностей.
Правило: Не зберігайте адреси змінних в файлах. Змінні, які спочатку знаходилися за одними адресами, при повторному запуску програми можуть перебувати вже за іншими адресами.