Текстовые файлы и списки

Зачем файлы

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

В ЛР3 используются два фиксированных файла:

  • input.txt — входные данные;
  • output.txt — результат.

Данные — только числа. Строковые функции и массивы char как данные не используются.

Открытие и закрытие

#include <stdio.h>

FILE *in = fopen("input.txt", "r");
if (in == NULL)
{
    perror("input.txt");
    return 1;
}

/* ... чтение ... */

fclose(in);

Режимы:

РежимЧто делает
"r"чтение
"w"запись, файл создаётся/перезаписывается
"a"дописывание в конец

Для output.txt:

FILE *out = fopen("output.txt", "w");
if (out == NULL)
{
    perror("output.txt");
    return 1;
}

/* ... запись ... */

if (fclose(out) != 0)
{
    return 1;
}

fclose тоже может завершиться ошибкой, особенно для записи. Проверяйте результат.

Чтение чисел

int n;
if (fscanf(in, "%d", &n) != 1 || n < 0)
{
    fclose(in);
    return 1;
}

Для double используйте %lf:

double score;
if (fscanf(in, "%lf", &score) != 1)
{
    return 1;
}

fscanf возвращает число успешно прочитанных полей. Сравнивайте с ожидаемым количеством, а не с EOF напрямую.

Запись чисел

fprintf(out, "%d %d %.2f\n", student.id, student.course, student.score);

Для double в fprintf используется %f или %.2f (не %lf).

Чтение списка из файла

typedef struct
{
    int id;
    int course;
    double score;
} Student;

typedef struct Node
{
    Student data;
    struct Node *next;
} Node;

Node *load_list(FILE *in, int n)
{
    Node *head = NULL;
    Node *tail = NULL;

    for (int i = 0; i < n; i++)
    {
        Student s;
        if (fscanf(in, "%d %d %lf", &s.id, &s.course, &s.score) != 3)
        {
            /* освободить уже созданные узлы */
            return NULL;
        }

        Node *node = malloc(sizeof(*node));
        if (node == NULL)
        {
            /* освободить список */
            return NULL;
        }

        node->data = s;
        node->next = NULL;

        if (tail == NULL)
        {
            head = tail = node;
        }
        else
        {
            tail->next = node;
            tail = node;
        }
    }

    return head;
}

В реальной функции при ошибке нужно освободить уже созданные узлы, а не просто вернуть NULL. Это важно, иначе получится утечка.

Запись списка в файл

void save_list(FILE *out, const Node *head)
{
    for (const Node *p = head; p != NULL; p = p->next)
    {
        fprintf(out, "%d %d %.2f\n", p->data.id, p->data.course, p->data.score);
    }
}

Обработка ошибок

Проверяйте:

  • fopen — файл существует и доступен;
  • fscanf — прочитано ожидаемое число полей;
  • malloc — память выделена;
  • fprintf/fclose — запись завершилась успешно.

При ошибке:

  1. вывести диагностику в stderr/консоль;
  2. освободить память;
  3. закрыть открытые файлы;
  4. вернуть ненулевой код.

Результат в штатном режиме пишется только в output.txt.

Частые ошибки

ОшибкаЧто не так
не проверили fopenработа с NULL
%f вместо %lf в fscanf для doubleневерное чтение
%lf в fprintf для doubleлишний l, но обычно допустим; лучше %f
не проверили fscanf«мусор» в данных
не закрыли файлпотеря данных при записи
не проверили fcloseошибка записи осталась незамеченной
результат вывели в консольтребование не выполнено
забыли освободить список при ошибкеутечка

Документация

Что запомнить

  1. fopen открывает файл, fclose закрывает; проверяйте оба.
  2. fscanf/fprintf работают как scanf/printf, но с FILE *.
  3. Для double при чтении — %lf, при записи — %f.
  4. Загрузка списка из файла — это чтение записей и создание узлов.
  5. При ошибке освободите память и верните ненулевой код.

Подтверждение

—