На этом уроке мы рассмотрим реализацию рандомного файлового ввода/вывода в языке С++.
Файловый указатель
Каждый класс файлового ввода/вывода содержит файловый указатель, который используется для отслеживания текущей позиции чтения/записи данных в файле. Любая запись в файл или чтение содержимого файла происходит в текущем местоположении файлового указателя. По умолчанию, при открытии файла для чтения или записи, файловый указатель находится в самом начале этого файла. Однако, если файл открывается в режиме добавления, то файловый указатель перемещается в конец файла, чтобы пользователь имел возможность добавить данные в файл, а не перезаписать его.
Рандомный доступ к файлам с помощью функций seekg() и seekp()
До этого момента мы осуществляли последовательный доступ к файлам, т.е. выполняли чтение/запись файла по порядку. Тем не менее, мы можем выполнить и произвольный (рандомный) доступ к файлу (т.е. перемещаться по файлу, как захотим). Это может быть полезно, когда файл имеет обширное содержимое, а нам нужна всего лишь небольшая конкретная запись из всего этого. Вместо последовательного доступа (когда мы переходим до нужной записи начиная с самого начала файла), мы можем осуществить непосредственный доступ к этой записи.
Рандомный доступ к файлу осуществляется путем манипулирования файловым указателем с помощью функции seekg() (окончание «g« = «get», т.е. «получить/достать») — для ввода, и функции seekp() (окончание «p« = «put» (т.е. «положить/поместить») — для вывода.
Функции seekg() и seekp() принимают следующие два параметра:
первый параметр — это смещение на которое следует переместить файловый указатель (измеряется в байтах);
второй параметр — это флаг ios, который обозначает место, от которого следует отталкиваться при выполнении смещения.
Флаги ios, которые принимают функции seekg() и seekp() в качестве второго параметра:
beg — cмещение относительно начала файла (по умолчанию);
cur — cмещение относительно текущего местоположения файлового указателя;
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
, приведет к неприятностям.
Правило: Не сохраняйте адреса переменных в файлах. Переменные, которые изначально были по одним адресам, при повторном запуске программы могут находиться уже по другим адресам.
Большое спасибо за статью!!!
Подскажите, пожалуйста, почему моя программа записывает новую информацию (букву щ) в самый конец файла, хотя я хочу, чтобы она записывала в ту позицию куда я указал (iofile.seekp(pos,ios_base::beg);)
Почему используется для получения нынешнего местоположения:
а не:
Я не понимаю, зачем лишний раз вызывать метод, это конечно маленькие затраты, но все равно на одну машинную команду больше (а если она является переопределенным виртуальным методом, так и вовсе три команды), да и читаемость лишний раз понижается. Есть какаята явная причина?
спасибо, вообще в тему, пол дня потратил, а тут все написано, блин, нужно было просто пролистать ниже.
Добрый день! В абзаце "Рандомный доступ к файлу осуществляется путём манипулирования файловым указателем с помощью методов seekg() (для ввода) и seekp() (для вывода). В seekg() окончание «g» = “get” (т.е. «получить/достать»), в seekp() окончание «p» = «put» (т.е. «положить/поместить»)." — нет ошибки? У меня методы работают наоборот.