Операция чтения-записи всегда производится с текущей позиции в потоке. При открытии потока в режимах 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!"
Кроме функции 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