17.5.3. Произвольный доступ к потоку
Некоторые из потоковых классов обеспечивают произвольный доступ к данным связанного с ними потока. Положение в потоке можно изменить так, чтобы прочитать сначала последнюю строку, затем первую и т.д. Для установки (seek) необходимой позиции и сообщения (tell) текущей позиции в потоке библиотека предоставляет пару функций.
Произвольный доступ для чтения и записи напрямую зависит от системы. Чтобы выяснить способ применения этой возможности, следует обратиться к документации на систему.
Хотя функции seek() и tell() определены для всех потоковых классов, возможные для них действия определяются видом объекта, с которым связан поток. В большинстве систем поток, с которым связан потоковый объект cin, cout, cerr или clog, не обеспечивает возможности произвольного доступа — в конце концов, как можно перейти на десять позиций обратно, если запись осуществляется непосредственно в объект cout? Применить функции seek() и tell(), конечно, можно, но во время выполнения это приведет к ошибке и переходу потока в недопустимое состояние.
Поскольку классы istream и ostream обычно не обеспечивают произвольного доступа, в остальной части этого раздела речь идет только о классах fstream и sstream.
Функции установки и сообщения
Для обеспечения произвольного доступа типы ввода-вывода обладают маркером (marker), который указывает позицию следующей операции чтения или записи. Они обладают также двумя функциями: одна устанавливает (seek) маркер в новую позицию, а вторая сообщает (tell) текущую позицию маркера. Фактически в библиотеке определены две пары функций установки и сообщения, которые описаны в табл. 17.21. Одна пара функций используется потоками ввода, а вторая — потоками вывода. Версии для ввода и вывода различаются суффиксом. Суффикс g (getting) означает получение данных (чтение), а суффикс p (putting) — помещение данных (запись).
Таблица 17.21. Функции установки и сообщения
tellg() tellp() Возвращает текущую позицию маркера потока ввода (tellg()) или потока вывода (tellp()) seekg(pos) seekp(pos) Переустанавливает маркер потока ввода или вывода на заданный параметром pos абсолютный адрес в потоке. Значение pos обычно возвращается предыдущим вызовом в соответствующей функции tellg() или tellp() seekp(off, from) seekg(off, from) Переустанавливает маркер потока ввода или вывода на off символов вперед или назад от значения from, которое может быть: beg — от начала потока; cur — от текущей позиции потока; end — от конца потокаВполне логично, что для класса istream, а также производных от него классов ifstream и istringstream (см. раздел 8.1) можно использовать только версии g, а для классов ostream и классов ofstream и ostringstream, производных от него, можно использовать только версии p. Классы iostream, fstream и stringstream способны читать и записывать данные в поток, поэтому для них можно использовать обе версии, g и p.
Существует только один маркер
Тот факт, что библиотека различает версии функций seek() и tell() для чтения и записи, может ввести в заблуждение. Хотя библиотека и различает эти функции, в файле существует только один маркер, т.е. нет разных маркеров для чтения и записи.
Когда речь идет о потоке только ввода или вывода, различие не столь очевидно. В таких потоках можно использовать версии только g или p. Если попытаться вызвать функцию tellp() для объекта класса ifstream, компилятор сообщит об ошибке. Аналогично он поступит при попытке вызвать функцию seekg() для объекта класса ostringstream.
Типы fstream и stringstream допускают чтение и запись в тот же поток. У них есть один буфер для хранения подлежащих чтению и записи данных, а также один маркер, обозначающий текущую позицию в буфере. Библиотечные функции версий g и p используют тот же маркер позиции.
Поскольку существует только один маркер, для переустановки маркера при каждом переключении между чтением и записью следует применять функцию seek().
Перемещение маркера
Имеются две версии функции установки позиции: одна обеспечивает переход к указанной позиции в файле, а другая осуществляет смещение от текущей позиции.
// установка маркера в заданную позицию
seekg(new_position); // установить маркер чтения в позицию pos_type
seekp(new_position); // установить маркер записи в позицию pos_type
// смещение позиции на указанную дистанцию от текущей
seekg(offset, from); // установить дистанцию смещения маркера чтения
seekp(offset, from); // от from; offset имеет тип off_type
Возможные значения параметра from перечислены в табл. 17.21.
Аргументы new_position и offset этих функций имеют машинно-зависимые типы pos_type и off_type соответственно. Они определены в классах istream и ostream. Тип pos_type представляет позицию файла, а тип off_type — смещение от этой позиции. Значение типа off_type может быть положительным или отрицательным, что соответствует смещению вперед или назад.
Доступ к маркеру
Функции tellg() и tellp() возвращают значение типа pos_type, обозначающее текущую позицию в потоке. Эти функции обычно используются для того, чтобы запомнить позицию и впоследствии вернуться к ней:
// запомнить текущую позицию записи в переменную mark
ostringstream writeStr; // поток вывода в строку
ostringstream::pos_type mark = writeStr.tellp();
// ...
if (cancelEntry)
// возврат к отмеченной позиции
writeStr.seekp(mark);
Чтение и запись в тот же файл
Рассмотрим пример программы, которая читает файл и записывает в его конец новую строку, содержащую относительную позицию начала каждой строки. Предположим, например, что работать придется со следующим файлом.
Abcd
efg
hi
j
Модифицированный программой файл должен выглядеть следующим образом.
Abcd
efg
hi
j
5 9 12 14
Обратите внимание: программа не записывает смещение для первой строки, она всегда начинается с позиции 0. Обратите также внимание на то, что смещения должны также учитывать невидимый символ новой строки, завершающий каждую строку. И наконец, последнее число в выводе — смещение для строки, с которой начинается вывод. При включении этого смещения в вывод можно отличить свой вывод от первоначального содержимого файла. Можно прочитать последнее число в полученном файле и установить смещение так, чтобы получить позицию начала вывода.
Наша программа будет читать файл построчно. Для каждой строки значение счетчика будет увеличиваться на размер только что прочитанной строки. Этот счетчик содержит смещение, с которого начинается следующая строка:
int main() {
// открыть файл для ввода и вывода, а затем перейти в его конец
// аргументы режима файла приведены в табл. 8.4
fstream inOut("copyOut",
fstream::ate | fstream::in | fstream::out);
if (!inOut) {
cerr << "Unable to open file!" << endl;
return EXIT_FAILURE; // EXIT_FAILURE см. p. 6.3.2
}
// inOut открыт в режиме ate, поэтому исходной позицией файла будет
// его конец
auto end_mark = inOut.tellg(); // запомнить позицию первоначального
// конца файла
inOut.seekg(0, fstream::beg); // перейти к началу файла
size_t cnt = 0; // счетчик количества байтов
string line; // содержит каждую строку ввода
// пока нет ошибки и исходные данные читаются
while (inOut && inOut.tellg() != end_mark
&& getline(inOut, line)) { // и можно получить следующую строку
cnt += line.size() + 1; // добавить 1 для новой строки
auto mark = inOut.tellg(); // запомнить позицию чтения
inOut.seekp(0, fstream::end); // установить маркер записи в конец
inOut << cnt; // записать общую длину
// вывести разделитель, если это не последняя строка
if (mark != end_mark) inOut << " ";
inOut.seekg(mark); // восстановить позицию чтения
}
inOut.seekp(0, fstream::end); // перейти к концу
inOut << " "; // вывести символ новой строки в конце файла
return 0;
}
Эта программа открывает поток fstream в режимах in, out и ate (см. табл. 8.4). Первые два режима означают, что предполагается чтение и запись в тот же файл. Режим ate означает, что начальной позицией открытого файла будет его конец. Как обычно, необходимо удостовериться, что файл открыт корректно, если это не так, следует выйти из программы (см. раздел 6.3.2).
Поскольку программа пишет в свой исходный файл, нельзя использовать конец файла как признак прекращения чтения. Цикл должен закончиться по достижении конца первоначального ввода. В результате сначала следует запомнить первоначальную позицию конца файла. Так как файл открыт в режиме ate, поток inOut уже установлен в конец. Сохраним текущую (т.е. первоначальную) позицию конца файла в переменной end_mark. Запомнив конечную позицию, маркер чтения следует установить в начало файла, чтобы можно было приступить к чтению данных.
Цикл while имеет три условия выхода: сначала проверяется допустимость потока; если это так, то проверяется, не достигнут ли конец исходных данных. Для этого текущая позиция чтения, возвращаемая функцией tellg(), сравнивается с позицией, заранее сохраненной в переменной end_mark. И наконец, если обе проверки пройдены успешно, происходит вызов функции getline(), которая читает следующую строку из файла. Если вызов функции getline() успешен, выполняется тело цикла.
Тело цикла начинается с запоминания текущей позиции в переменной mark. Она сохраняется для возвращения после записи следующего относительного смещения. Вызов функции seekp() переводит маркер записи в конец файла. Выводится значение счетчика, а затем функция seekg() возвращается к позиции, сохраненной в переменной mark. Восстановив положение маркера, можно снова проверить условие выхода из цикла while.
Каждая итерация цикла выводит смещение следующей строки. Поэтому последняя итерация цикла заботится о записи смещения последней строки. Однако в конец файла следует еще записать символ новой строки. Как и в других случаях записи, для позиционирования в конец файла перед выводом новой строки происходит вызов функции seekp().
Упражнения раздела 17.5.3
Упражнение 17.39. Напишите собственную версию программы, представленной в этом разделе.
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК