Позиционирование в файле

Последнее обновление: 06.09.2023

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

Однако вполне возможно, что нам потребуется считывать или записывать с какой-то определенной позиции в файле. Например, в айдиофайле в формате wav собственно звуковые данные расположены, начиная с 44 байта. И если, к примеру, мы хотим распознать звук из файла, что-то с ним сделать, то нам при считывании данных надо переместить указатель на соответствующую позицию.

В языке Си для управления позицией указателя в потоке применяется функция fseek(), которая имеет следующий синтаксис:

int fseek(указатель_на_поток, смещение, начало_отсчета);

Второй параметр этой функции - смещение представляет числовое значение типа long, которое указывает, на какое количество байт надо переместить указатель. Это значение может быть отрицательным, если необходимо в потоке вернуться назад на некоторое количество байт.

Третий параметр - начало_отсчета задает начальную позицию, относительно которой идет смещение. В качестве этого параметра мы можем использовать одну из встроенных констант, определенных в файле stdio.h:

  • SEEK_SET: имеет значение 0 и представляет начало файла

  • SEEK_CUR: имеет значение 1 и представляет текущую позицию в потоке

  • SEEK_END: имеет значение 2 и представляет конец файла

Если перемещение указателя было успешно выполнено, то функция fseek() возвращает 0, иначе она возвращает ненулевое значение.

Применим функцию fseek в программе:

#include <stdio.h>

void load(char *, int);
void save(char *);
int main(void)
{
    // файл для записи и чтения
    char * filename = "data.txt";
    // позиция, с которой начинается считывание
    int position = 6;
    save(filename);
    load(filename, position);
    
    return 0;
}
void load(char * filename, int position)
{
    // буфер для считавания данных из файла
    char buffer[256];
    // чтение из файла
    FILE *fp = fopen(filename, "r");
    if(!fp)
    {
        printf("Error occured while opening file\n");
        return;
    }
    // перемещаем указатель в файле на позицию position
    fseek(fp, position, SEEK_SET);
    // пока не дойдем до конца, считываем по 256 байт
    while((fgets(buffer, 256, fp)))
    {
        printf("%s", buffer);
    }
     
    fclose(fp);
}
void save(char * filename)
{
    // строка для записи
    char * message = "Hello METANIT.COM!";
    // запись в файл
    FILE *fp = fopen(filename, "w");
    if(!fp)
    {
        printf("Error occured while opening file\n");
        return;
    }
    // записываем строку
    fputs(message, fp);
 
    fclose(fp);
    printf("File has been written\n");
}

Здесь функция save() записывает в файл строку "Hello METANIT.COM!". Далее функция load() считывет данные из этой строки. Но считывает не сначала, а с позиции, которая передается через параметр position:

// перемещаем указатель в файле на позицию position
fseek(fp, position, SEEK_SET);

Поскольку третий аргумент равен SEEK_SET, то указатель файла смещается к байту с индексом position. Соответственно далее функция fgets() будет считывать данные не с самого начала файла, а с позиции position:

while((fgets(buffer, 256, fp)))

Поскольку в данном случае в качестве позиции передается число 6, то в тексте файла будут пропущены первые 6 символов, и будет считана подстрока "METANIT.COM!"

ftell

Кроме функции fseek() мы можем использовать для управления позицией указателя еще пару функций:

  • long ftell(FILE *): получает текущую позицию указателя

  • void rewind(FILE *): указатель устанавливается на начало потока

Например, мы можем применять функцию ftell для вычисления длины файла в байтах:

#include <stdio.h>

int main(void){
    
    FILE* fp = fopen("test.txt", "r");
    if(!fp) // если не удалось открыть файл
    {
        printf("Error while opening file\n");
        return -1;
    }
    // если удалось, открыть файл получаем его длину
    fseek(fp, 0, SEEK_END);  // устанавливаем указатель на конец файл
    long size = ftell(fp);   // получаем значение указателя относительно начала
    fclose(fp);     // закрываем файл

    printf("File size: %ld bytes\n", size);
}  

В данном случае находим длину файла "text.txt", который располагается в папке программы. Само находждение длины разбивается на два этапа. Сначала помещаем указатель в файле на конец с помощью функции fseek():

fseek(fp, 0, SEEK_END);

Затем с помощью функции ftell() вычисляем положение указателя относительно начала файла - фактически получаем размер файла в байтах

long size = ftell(fp);

Считывание определенной структуры из файла

Рассмотрим более сложный пример:

#include <stdio.h>
#include <stdlib.h>
  
struct person
{
    char name[20];
    int age;
};
  
int save(char * filename, struct person *st, int n);
int load(char * filename);
  
int main(void)
{
    char * filename = "people.dat";
    struct person people[] = { {"Tom", 23}, {"Alice", 27}, {"Bob", 31}, {"Kate", 29} };
    int n = sizeof(people) / sizeof(people[0]);
  
    save(filename, people, n);
    load(filename);
     
    return 0;
}
  
// запись в файл массива структур
int save(char * filename, struct person * st, int n)
{
    char *c;    // указатель для посимвольной записи данных
    // число записываемых байтов
    int size = n * sizeof(struct person);
      
    FILE * fp = fopen(filename, "wb");
    if (!fp)
    {
        printf("Error occured while opening file\n");
        return 1;
    }
    // записываем количество структур
    c = (char *)&n;
    for (int i = 0; i < sizeof(n); i++)
    {
        putc(*c++, fp);
    }
  
    // посимвольно записываем в файл все структуры
    c = (char *)st;
    for (int i = 0; i < size; i++)
    {
        putc(*c, fp);
        c++;
    }
    fclose(fp);
    return 0;
}
  
// загрузка из файла массива структур
int load(char * filename)
{
    char *c;    // указатель на считывания очередного символа
    int n = sizeof(struct person);  // сколько байт надо считать для структуры
    int index;  // номер структуры из файла:
    printf("Enter user number: ");  // ввод номера структуры
    scanf("%d", &index);
 
    FILE * fp = fopen(filename, "rb"); // открываем файл на чтение
    if (!fp)
    {
        printf("Error occured while opening file\n");
        return 1;
    }

    // выделяем память для количества структур
    int *ptr_count = malloc(sizeof(int));
    // считываем количество структур
    c = (char *)ptr_count;
    int m = sizeof(int);    // сколько надо считать структур
    // пока не считаем m байт
    while (m > 0 && (*c = getc(fp))!=EOF)  // сохраняем байт в выделенный блок для размера массива
    {
        c++;
        m--;
    }
    //получаем число элементов
    int count = *ptr_count;
    free(ptr_count);    // освобождаем память
    // если номер запрощенной структуры меньше кол-ва структур
    if(index > count)
    {
        printf("User number out of range\n");
        fclose(fp);
        return 1;
    }

    // получаем, на сколько байтов надо перемотать указатель относительно начала позиции
    int pos = (index-1) * n + 4;
    // перемещаем указатель на нужную позицию
    fseek(fp, pos, SEEK_SET);
     
    // выделяем память для считываемой структуры
    struct person * ptr = malloc(sizeof(struct person));

    // устанавливаем указатель на начало блока памяти
    c = (char *)ptr;
    // после записи считываем посимвольно из файла
    while(n > 0 && (*c=getc(fp))!=EOF)
    {
        c++;
        n--;
    }
    // вывод считанных данных на консоль
    printf("%-10s %5d \n", ptr->name, ptr->age);
    free(ptr);    
    fclose(fp);
    return 0;
}

С помощью функции save в файл сохраняется массив структур. Затем в функции load считываем одну из структур по введенному номеру.

Для поиска нужной структуры вычисляем позицию:

int pos = (index-1) * n + 4;

Исходя из записи мы знаем, что первые 4 байта в файле отводятся под хранение количества структур, соответственно при чтении нам надо пропустить эти 4 байта.

Например, мы хотим получить первую структуру. По формуле получаем (index-1) * n + 4 = 4. То есть первая структура будет располагаться после 4-го байта. Аналогично вторая структура будет располагаться после n+4 байт, где n - это размер структуры.

Получив позицию, передаем ее в функцию fseek(), перемещаемся в потоке и считываем после этого n байт.

// перемещаем указатель на нужную позицию
fseek(fp, pos, SEEK_SET);

При последующей операции чтения с помощью функции getc():

while(n > 0 && (*c=getc(fp))!=EOF)

эта функция будет считывать данные с позиции pos, по которой располагается структура с номером index.

Результат работы программы:

Input user number: 2
Alice			27

Стоит отметить, что для большей краткости здесь убрана проверка на наличие структуры по введенному номеру, так как пользователь может ввести число, которое превышает количество

Обновление структуры в файле

Мы можем использовать указатель в потоке не только для чтения, но и для записи данных. Например:

#include <stdio.h>

struct person
{
    unsigned id;
    char name[10];
    int age;
};

void save(char*);
void update_age(char*, int, int);

int main() {
    char * filename = "people.bin";
    save(filename);
    update_age(filename, 2, 33); // в структуре с id=2 устанавливаем age = 33
}
void save(char* filename)
{
    struct person people[] = { {1, "Tom", 38}, {2, "Sam", 25}, {3, "Bob", 42}};
    int size = sizeof(people[0]);              // размер всего массива
    int count = sizeof(people)  / size;         // количество структур 
    
    // запись файла
    FILE *fp = fopen(filename, "w");
    // записываем массив структур
    fwrite(people, size, count, fp); 
    fclose(fp);
    printf("%d people saved\n", count);
}

void update_age(char* filename, int id, int age)
{
    // считывание файла, пока не найдем структуру с определенным id
    struct person p;  // структура для чтения 
    int size = sizeof(p);
    FILE* fp = fopen(filename, "r+");	// "r+" - открываем файл для изменения
    // считываем данные в структуру
    while(fread(&p, sizeof(p), 1, fp)==1)
    {
        // если нашли структуру с нужным id
        if(p.id == id)
        {
            p.age = age;                    // изменяем возраст
            fseek(fp, -1*size, SEEK_CUR);   // перемещаем в потоке на один объект назад
            fwrite(&p, size, 1, fp);      // записываем обновленную структуру 
            break;                        // выходим из цикла
        }
    }
    rewind(fp);     // перематываем файл назад

    // считываем по одной структуре и проверяем изменения
    while(fread(&p, sizeof(p), 1, fp) == 1)
    { 
        printf("Id: %d \t Name: %s \t Age: %d \n", p.id, p.name, p.age);
    }
    fclose(fp);
}

В функции update_age() обновляем возраст пользователя по определенному id. Для этого открываем файл с флагом "r+", то есть для чтения с измением.

FILE* fp = fopen(filename, "r+");

Затем считываем по одной структуре и проверяем id. Если id структуры равен запрошенному id, то изменяем возраст и перезаписываем структуру в файле:

if(p.id == id)
{
    p.age = age;                    // изменяем возраст
    fseek(fp, -1*size, SEEK_CUR);   // перемещаем в потоке на один объект назад
    fwrite(&p, size, 1, fp);      // записываем обновленную структуру 
    break;                        // выходим из цикла
}

Для перезаписи перемещаемся в файле на начало структуры (fseek(fp, -1*size, SEEK_CUR)). Поскольку, когда мы считали структуру, указатель в файле указывает на следующую за ней структуру.

Поскольку id у структур уникальны, соответственно нам надо изменить только один объект, поэтому после изменения объекта выходим из цикла.

Чтобы до закрытия файла вывести в этой же функции все структуры из файла, перемещаем указатель в файле на начало с помощью функции rewind()

rewind(fp);     // перематываем файл назад

Таким образом, структура с id=2, где изначально age был равен 27, изменит это значение age на 33. Консольный вывод программы:

3 people saved
Id: 1    Name: Tom       Age: 38
Id: 2    Name: Sam       Age: 33
Id: 3    Name: Bob       Age: 42
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850