23.7 – Произвольный файловый ввод/вывод

Добавлено 12 октября 2021 в 23:17

Файловый указатель

Каждый класс файлового потока содержит файловый указатель, который используется для отслеживания текущей позиции чтения/записи в файле. Когда что-то читается или записывается в файл, чтение/запись происходит в текущем местоположении этого файлового указателя. По умолчанию при открытии файла для чтения или записи файловый указатель устанавливается на начало файла. Однако если файл открывается в режиме добавления, файловый указатель перемещается в конец файла, и поэтому какое-либо текущее содержимое файла не перезаписывается.

Произвольный доступ к файлам с помощью seekg() и seekp()

До сих пор весь доступ к файлам, который мы выполняли, был последовательным, то есть мы читали или записывали содержимое файла по порядку. Однако также к файлу можно выполнять произвольный доступ, то есть переходить к различным точкам в файле, чтобы прочитать его содержимое. Это может быть полезно, когда ваш файл содержит много записей, и вы хотите получить определенную из них. Вместо того чтобы читать все записи, пока не дойдете до нужной, вы можете сразу перейти к записи, которую хотите получить.

Произвольный доступ к файлу осуществляется путем манипулирования файловым указателем с помощью функции seekg() (для ввода) и функции seekp() (для вывода). Если вам интересно, g означает «get» (получить), а p – «put» (положить). Для некоторых типов потоков seekg() (изменение позиции чтения) и seekp() (изменение позиции записи) работают независимо, однако с файловыми потоками позиции чтения и записи всегда идентичны, поэтому seekg и seekp могут использоваться взаимозаменяемо.

Функции seekg() и seekp() принимают два параметра. Первый параметр – это смещение, определяющее, на сколько байтов переместить файловый указатель. Второй параметр – это флаг ios, который указывает, от чего должен быть смещен параметр смещения.

Флаг поиска iosЗначение
begСмещение относительно начала файла (по умолчанию)
curСмещение относительно текущего местоположения файлового указателя
endСмещение относительно конца файла

Положительное смещение означает перемещение файлового указателя к концу файла, тогда как отрицательное смещение означает перемещение файлового указателя к началу файла.

Вот несколько примеров:

inf.seekg(14, ios::cur);  // перейти вперед на 14 байтов
inf.seekg(-18, ios::cur); // перейти назад на 18 байтов
inf.seekg(22, ios::beg);  // перейти к 22-му байту в файле
inf.seekg(24);            // перейти к 24-му байту в файле
inf.seekg(-28, ios::end); // перейти к 28-му байту с конца файла

Перейти к началу или концу файла очень просто:

inf.seekg(0, ios::beg); // перейти к началу файла
inf.seekg(0, ios::end); // перейти в конец файла

Давайте рассмотрим пример, используя seekg() и входной файл, который мы создали в прошлом уроке. Этот входной файл выглядит так:

This is line 1
This is line 2
This is line 3
This is line 4

А вот сам пример:

int main()
{
    std::ifstream inf{ "Sample.dat" };

    // Если мы не смогли открыть входной файловый поток для чтения
    if (!inf)
    {
        // Распечатываем ошибку и выходим
        std::cerr << "Uh oh, Sample.dat could not be opened for reading!\n";
        return 1;
    }

    std::string strData;

    inf.seekg(5);             // переход к 5-му символу
    // Получаем оставшуюся часть строки и распечатываем ее
    getline(inf, strData);
    std::cout << strData << '\n';

    inf.seekg(8, ios::cur);   // перемещаемся вперед еще на 8 байтов
    // Получаем оставшуюся часть строки и распечатываем ее
    std::getline(inf, strData);
    std::cout << strData << '\n';

    inf.seekg(-15, ios::end); // перемещаемся на 15 байт от конца файла
    // Получаем оставшуюся часть строки и распечатываем ее
    std::getline(inf, strData);
    std::cout << strData << '\n';

    return 0;
}

Этот код дает следующий результат:

is line 1
line 2
his is line 4

Примечание. Некоторые компиляторы содержат ошибки в реализации seekg() и seekp() при использовании вместе с текстовыми файлами (из-за буферизации). Если ваш компилятор является одним из них (о чем вы узнаете, если ваш вывод будет отличаться от приведенного выше), вы можете вместо этого попробовать открыть файл в двоичном режиме:

ifstream inf("Sample.dat", ifstream::binary);

Еще две полезные функции – это tellg() и tellp(), которые возвращают абсолютную позицию файлового указателя. Их можно использовать для определения размера файла:

std::ifstream inf("Sample.dat");
inf.seekg(0, std::ios::end);    // перейти в конец файла
std::cout << inf.tellg();

Этот код печатает:

64

Это длина файла sample.dat в байтах (при условии возврата каретки после последней строки).

Одновременные чтение и запись файла с помощью fstream

Класс fstream может читать и записывать файл одновременно (почти)! Самое большое неудобство здесь в том, что невозможно переключаться между чтением и записью произвольно. После чтения или записи единственный способ переключиться между ними – выполнить операцию, изменяющую позицию файла. Если вы на самом деле не хотите перемещать файловый указатель (потому что он уже находится в нужном вам месте), вы всегда можете перейти к текущей позиции:

// предполагаем, что iofile - это объект типа fstream
iofile.seekg(iofile.tellg(), ios::beg); // переходим к текущей позиции в файле

Если вы этого не сделаете, может произойти множество странных и причудливых вещей.

Примечание: хотя может показаться, что iofile.seekg(0, ios::cur) также будет работать, похоже, что некоторые компиляторы могут оптимизировать этот вызов.

Еще одна хитрость: в отличие от ifstream, где мы могли бы сказать while(inf), чтобы определить, есть ли что-то еще для чтения, с fstream это работать не будет.

Давайте напишем пример файлового ввода/вывода с помощью fstream. Мы собираемся написать программу, которая открывает файл, читает его содержимое и заменяет любые найденные гласные на символ '#'.

int main()
{
    // Обратите внимание, что мы должны указать как in,
    // так и out, потому что мы используем fstream
    std::fstream iofile{ "Sample.dat", ios::in | ios::out };

    // Если не удалось открыть iofile, выводим ошибку
    if (!iofile)
    {
        // Распечатываем ошибку и выходим
        std::cerr << "Uh oh, Sample.dat could not be opened!\n";
        return 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, std::ios::cur);

                // Поскольку мы выполнили переход,
                // теперь мы можем безопасно выполнить запись,
                // поэтому давайте перезапишем гласную символом #
                iofile << '#';

                // Теперь мы хотим вернуться в режим чтения,
                // чтобы следующий вызов get() сработал правильно.
                // Мы вызовем seekg() для текущего местоположения,
                // потому что не хотим перемещать файловый указатель.
                iofile.seekg(iofile.tellg(), std::ios::beg);

                break;
        }
    }

    return 0;
}

Другие полезные функции работы с файлами

Чтобы удалить файл, просто используйте функцию remove().

Функция is_open() вернет true, если поток в данный момент открыт, и false в противном случае.

Предупреждение о записи указателей на диск

Хотя потоковая передача переменных в файл довольно проста, всё становится сложнее, когда вы имеете дело с указателями. Помните, что указатель просто содержит адрес переменной, на которую он указывает. Хотя адреса можно читать и записывать на диск, это чрезвычайно опасно. Это связано с тем, что адрес переменной при каждом выполнении может различаться. Следовательно, хотя переменная могла существовать по адресу 0x0012FF7C, когда вы записывали этот адрес на диск, она может больше не находиться там, когда вы читаете этот адрес обратно!

Например, предположим, что у вас есть целое число с именем nValue, которое находится по адресу 0x0012FF7C. Вы присвоили nValue значение 5. Вы также объявили указатель с именем *pnValue, который указывает на nValue. pnValue содержит адрес nValue0x0012FF7C. Вы хотите сохранить их на потом, поэтому вы записываете на диск значение 5 и адрес 0x0012FF7C.

Через несколько недель вы снова запускаете программу и считываете эти значения с диска. Вы считываете значение 5 в другую переменную с именем nValue, которая находится по адресу 0x0012FF78. Вы считываете адрес 0x0012FF7C в новый указатель с именем *pnValue. Поскольку pnValue теперь указывает на 0x0012FF7C, а nValue находится на 0x0012FF78, pnValue больше не указывает на nValue, и попытка доступа к pnValue приведет к проблемам.

Правило: Не записывайте адреса в файлы. Переменные, которые изначально находились по этим адресам, могут находиться уже по другим адресам, когда вы будете считывать их значения обратно с диска, и эти адреса будут недействительными.

Теги

C++ / CppiostreamLearnCppseekg()seekp()std::fstreamstd::ifstreamstd::ofstreamSTL / Standard Template Library / Стандартная библиотека шаблоновВвод/выводДля начинающихОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.