Работа с файлами
Файл представляет собой произвольную последовательность данных. Содержимое файла может быть интерпретировано как последовательность текстовых символов (текстовые файлы) или как двоичные данные (бинарные файлы).
Для работы с файлами в Си используется стандартная библиотека stdio.h, которая предоставляет функции для открытия, чтения, записи и закрытия файлов. Основные функции для открытия и закрытия файлов:
fopen()
: открывает файл и возвращает указатель на него.fclose()
: закрывает файл, освобождая ресурсы и гарантирует, что все данные будут записаны на диск.
Пример открытия и закрытия файла:
#include <stdio.h>
int main(void){
FILE *f;
f = fopen("in.txt","w"); // открытие файла in.txt на запись
fclose(f); //Закрытие файла.
return 0;
}
Функция fopen()
принимает два аргумента: имя файла и режим открытия.
Текстовые файлы
в функции fopen()
режимы открытия для текстового файла могут быть:
- r – существующий текстовый файл открывается для чтения.
- w – создается новый текстовый файл и открывается на запись. Если файл с таким именем существовал ранее, то его содержимое удаляется.
- a – текстовый файл открывается для записи в конец файла (его "старое" содержимое сохраняется) или создается.
- r+ – существующий текстовый файл открывается для чтения и записи, текущая позиция (место в файле, по которому происходит чтение или запись) устанавливается в начало файла.
- w+ – текстовый файл открывается или создается для чтения и записи, текущая позиция устанавливается в начало файла. Если файл с таким именем существовал ранее, то его содержимое. удаляется
- а+ – текстовый файл открывается для чтения и записи, текущая позиция устанавливается в конец файла. Если файл не существовал, то он создаётся.
Чтение данных из текстовых файлов
Для чтения данных из текстовых файлов используются функции fgetc()
, fgets()
и fscanf()
. Каждая из них имеет свои особенности и применяется в разных ситуациях.
fgetc()
Функция fgetc()
возвращает следующий символ из файла или EOF, если достигнут конец файла или произошла ошибка. Использование этой функции удобно для чтения текстовых файлов посимвольно.
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Ошибка при открытии файла");
return 1;
}
int ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch); // Вывод символа на экран
}
fclose(file);
return 0;
}
В цикле while проверяется, не достигнут ли конец файла, и выводится каждый прочитанный символ на экран с помощью функции putchar()
.
fgets()
Функция fgets()
читает строку из файла и сохраняет ее в буфер. Она прекращает чтение при достижении конца строки или при заполнении буфера. Это делает fgets()
удобной для чтения строк переменной длины.
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Ошибка при открытии файла");
return 1;
}
char buffer[100];
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer); // Вывод строки на экран
}
fclose(file);
return 0;
}
В примере используется цикл while для чтения строк до конца файла и вывод каждой строки на экран с помощью функции printf()
.
fscanf()
Функция fscanf()
работает аналогично scanf()
, но читает данные из файла. Она принимает указатель на файл и строку формата, которая определяет, какие данные нужно прочитать. В примере читается целое число из текстового файла и выводится на экран. fscanf()
удобна для чтения структурированных данных, таких как таблицы или конфигурационные файлы.
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Ошибка при открытии файла");
return 1;
}
int number;
fscanf(file, "%d", &number);
printf("Прочитанное число: %d\n", number);
fclose(file);
return 0;
}
Запись данных в текстовые файлы
Запись данных в файлы осуществляется с помощью функций fputc()
, fputs()
, и fprintf()
.
fputc()
Функция fputc()
записывает один символ в файл и возвращает записанный символ или EOF в случае ошибки.
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
perror("Ошибка при открытии файла");
return 1;
}
fputc('A', file);
fclose(file);
return 0;
}
fputs()
Функция fputs()
записывает строку в файл и возвращает ненулевое значение в случае ошибки.
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
perror("Ошибка при открытии файла");
return 1;
}
fputs("Hello, World!\n", file);
fclose(file);
return 0;
}
fprintf()
Функция fprintf()
работает аналогично printf()
, но записывает данные в файл. Она принимает указатель на файл и строку формата, которая определяет, какие данные нужно записать.
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
perror("Ошибка при открытии файла");
return 1;
}
int number = 42;
fprintf(file, "Число: %d\n", number);
fclose(file);
return 0;
}
Бинарные файлы
Открытие и закрытие бинарных файлов
Для открытия файла в бинарном режиме к значению второго аргумента в функции fopen()
приписывается буква b
- wb - бинарный файл открывается для записи.
- rb - бинарный файл открывается для чтения
- ab - бинарный файл открывается для дозаписи
- w+b - бинарный файл создается для записи/чтения
- r+b - бинарный файл открывается для чтения/записи
- a+b - бинарный файл открывается или создается (при его отсутствии) для чтения/дозаписи
Пример:
FILE *file;
file = fopen("example.bin", "wb");
Чтение данных из бинарных файлов
fread()
Для чтения данных из бинарных файлов используется функция fread()
, которая работает с буфером, что позволяет читать данные блоками.
#include <stdio.h>
int main() {
FILE *file = fopen("example.bin", "rb");
if (file == NULL) {
perror("Ошибка при открытии файла");
return 1;
}
char buffer[100];
size_t bytesRead = fread(buffer, 1, sizeof(buffer), file);
printf("Прочитано %zu байт\n", bytesRead);
fclose(file);
return 0;
}
Функция fread()
читает блок данных из файла и сохраняет его в буфер. Она принимает четыре аргумента: указатель на буфер, размер одного элемента, количество элементов и указатель на файл. В примере выше читаются данные в буфер до 100 байт из бинарного файла и выводится количество прочитанных байт.
Запись данных в бинарные файлы
fwrite()
Для записи данных в бинарный файл используется функция fwrite()
, которая также как и fread()
работает с буфером.
#include <stdio.h>
int main() {
FILE *file = fopen("example.bin", "wb");
if (file == NULL) {
perror("Ошибка при открытии файла");
return 1;
}
char buffer[100] = "Пример данных";
size_t bytesWritten = fwrite(buffer, 1, sizeof(buffer), file);
printf("Записано %zu байт\n", bytesWritten);
fclose(file);
return 0;
}
В примере записываются данные из буфера до 100 байт в бинарный файл и выводится количество записанных байт.
Функции fread и fwrite обеспечивают высокую производительность за счет работы с буферами. Это позволяет минимизировать количество операций ввода-вывода, что особенно важно при работе с большими объемами данных. Например, вместо чтения или записи данных по одному байту, вы можете использовать буфер для обработки больших блоков данных за один раз.
Структуры и массивы в бинарных файлах
Работа с бинарными файлами становится особенно полезной при работе со сложными структурами данных, такими как структуры и массивы. Пример записи и чтения структуры:
typedef struct {
int id;
char name[50];
float salary;
} Employee;
Employee emp = {1, "John Doe", 50000.0};
fwrite(&emp, sizeof(Employee), 1, file);
Для чтения структуры из файла:
Employee readEmp;
fread(&readEmp, sizeof(Employee), 1, file);
Аналогично можно работать и с массивами:
int array[5] = {1, 2, 3, 4, 5};
fwrite(array, sizeof(int), 5, file);
Чтение массива:
int readArray[5];
fread(readArray, sizeof(int), 5, file);
Структуры и массивы позволяют организовать данные в удобные для обработки формы. Например, структура Employee может содержать информацию о сотруднике, такую как идентификатор, имя и зарплата. Запись и чтение таких структур из бинарного файла позволяет сохранять и восстанавливать сложные данные без необходимости преобразования их в текстовый формат.
typedef struct {
int id;
char name[50];
float salary;
} Employee;
Employee employees[3] = {
{1, "Alice", 60000.0},
{2, "Bob", 55000.0},
{3, "Charlie", 70000.0}
};
FILE *file = fopen("employees.bin", "wb");
fwrite(employees, sizeof(Employee), 3, file);
fclose(file);
file = fopen("employees.bin", "rb");
Employee readEmployees[3];
fread(readEmployees, sizeof(Employee), 3, file);
fclose(file);
Этот пример демонстрирует, как можно записывать и читать массив структур. Массив employees содержит три структуры Employee, каждая из которых представляет собой запись о сотруднике. Запись массива в файл и последующее его чтение позволяет сохранить и восстановить данные о сотрудниках.
Позиционирование в файле
fseek()
Операция чтения-записи всегда производится с текущей позиции в потоке. При открытии потока в режимах r и w указатель текущей позиции устанавливается на начальный байт потока. При открытии в режиме a указатель устанавливается на конец файла сразу за конечным байтом. И при выполнении операции чтения-записи указатель в потоке перемещается на новую позиции в соответствии с числом прочитанных или записанных байтов. Если требуется считывать или записывать с какой-то определенной позиции, то используется функция fseek()
, которая имеет следующий синтаксис:
int fseek(указатель_на_поток, смещение, начало_отсчета);
-
Второй параметр этой функции - смещение представляет числовое значение типа long, которое указывает, на какое количество байт надо переместить указатель. Это значение может быть отрицательным, если необходимо в потоке вернуться назад на некоторое количество байт.
-
Третий параметр - начало_отсчета задает начальную позицию, относительно которой идет смещение. В качестве этого параметра мы можем использовать одну из встроенных констант, определенных в файле stdio.h:
SEEK_SET: имеет значение 0 и представляет начало файла
SEEK_CUR: имеет значение 1 и представляет текущую позицию в потоке
SEEK_END: имеет значение 2 и представляет конец файла
Если перемещение указателя было успешно выполнено, то функция fseek() возвращает 0, иначе она возвращает ненулевое значение.
Пример:
#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 World!";
// запись в файл
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");
}
Поскольку в данном случае в качестве позиции передается число 6, то в тексте файла будут пропущены первые 6 символов, и будет считана подстрока "World!"
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);
Особенности позиционирования в файле
- Если новое положение в файле находится за текущим концом файла, и файл открыт на запись или чтение/запись, файл расширяется нулями до требуемого размера.
- Если новое положение в файле находится до начала файла, возвращается ошибка.
- Функции позиционирования могут быть неприменимы к стандартным потокам, потому что стандартные потоки могут быть связаны с устройствами, которые не допускают произвольное позиционирование (например, терминалы).
Обработка ошибок при работе с файлами
Работа с файлами может сопровождаться различными ошибками, такими как невозможность открытия файла или ошибки чтения/записи. Для обработки ошибок используются функции perror()
и feof()
.
perror()
Функция perror()
выводит сообщение об ошибке на стандартный вывод ошибок. В качестве аргумента функция принимает строку, которая будет выведена перед сообщением об ошибке.
#include <stdio.h>
int main() {
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
perror("Ошибка при открытии файла");
return 1;
}
fclose(file);
return 0;
}
В примере выше при попытке открыть несуществующий файл выводится сообщение об ошибке с помощью perror()
.
feof()
Функция feof()
возвращает ненулевое значение, если достигнут конец файла
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Ошибка при открытии файла");
return 1;
}
int ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
if (feof(file)) {
printf("\nДостигнут конец файла\n");
}
fclose(file);
return 0;
}
В примере выше читаются символы из файла до конца и проверяется, достигнут ли конец файла с помощью feof()
.
Советы по работе с файлами
- Всегда проверяйте возвращаемые значения функций для обнаружения ошибок.
- Используйте отладочные сообщения для отслеживания выполнения программы.
- Проверяйте правильность путей к файлам и прав доступа.
- Используйте функции
ferror()
иclearerr()
для проверки и сброса ошибок файловых операций. - Разделяйте логику работы с файлами на функции для улучшения читаемости и удобства отладки.
- Используйте буферы: Чтение и запись данных блоками может значительно повысить производительность. Буферизация позволяет минимизировать количество операций ввода-вывода, что особенно важно при работе с большими файлами.
- Проверяйте ошибки: Всегда проверяйте успешность выполнения операций с файлами. Это поможет избежать неожиданных сбоев и потери данных.
- Используйте правильные режимы: Убедитесь, что используете правильные режимы открытия файлов (rb, wb, ab). Неправильный выбор режима может привести к потере данных или повреждению файла.
- Работайте с указателями: Для работы с функциями
fread()
иfwrite()
необходимо использовать указатели на данные. Это позволяет эффективно управлять памятью и данными.
Самостоятельная работа
Задачи
Задача 1
Дан текстовый файл in.txt, содержащий целые числа. Посчитать сумму чисел.
Решение
FILE *f;
int sum = 0, n;
f = fopen("in.txt", "r");
while (fscanf (f, "%d", &n) == 1)
sum += n;
fclose (f);
printf ("%d\n", sum);
Задача 2
Ввести имя файла и напечатать его размер, используя функцию ftell
Решение
FILE *f;
static char filename[100]={0};
size_t size;
printf("Input file name: ");
scanf("%s",filename);
f = fopen (filename, "r");
if (f != NULL) {
fseek (f, 0, SEEK_END);
size = ftell (f);
fclose (f);
printf ("File size of '%s' - %lu bytes.\n",filename, size);
}
else {
printf ("Can't open file %s\n", filename);
}
Задача 3
Дан текстовый файл in.txt. Необходимо посчитать количество цифр в файле и записать это число в конец данного файла.
Решение
FILE *f;
int sum = 0, n;
signed char c;// обязательно signed! иначе зациклится
f = fopen("in.txt", "r+"); // режим чтение и дозапись
while ( (c=fgetc(f))!=EOF ) {
if(c>='0' && c<='9') {
sum += c-'0';
}
}
fprintf (f, " %d", sum);
fclose (f);