Раздел 13 — Структуры и текстовые файлы

Записи из нескольких полей, массивы записей и их хранение между запусками программы

Прогресс курса Раздел 13 из 28

Что вы освоите в этом разделе

2 академических часа теории и 2 часа практики (час в конце блока 1, час в начале блока 2): практическая работа №11 «Файлы последовательного доступа» — на портале. Раздел завершает блок 1; после него — экзамен по блоку 1.
01

Структура

Студент — это фамилия, группа и средний балл: значения разных типов, которые описывают один объект. Массив их не объединит (в нём элементы одного типа). Объединяет структура (в Pascal — запись):

enum { NAME_SIZE = 41, GROUP_SIZE = 11 };

typedef struct {
    char name[NAME_SIZE];          /* фамилия, до 40 символов */
    char group[GROUP_SIZE];        /* группа, до 10 символов */
    double score;                  /* средний балл */
} Student;

Student s = {"Ivanov", "21V", 4.75};
printf("%s %s %.2f\n", s.name, s.group, s.score);    /* Ivanov 21V 4.75 */
s.score = 4.80;
Student copy = s;                                     /* структуры присваиваются целиком */
Строку в поле записывают с контролем размера: snprintf(s.name, sizeof s.name, "%s", source) (раздел 11). sizeof s.name — размер массива-поля, 41, его можно вычислить там, где видна структура.
02

Массив структур и указатель на структуру

Группа студентов — массив структур: Student students[MAX_STUDENTS];, поле i-го — students[i].score. Сортировка по полю — любая сортировка раздела 8, где сравниваются поля, а обмениваются записи целиком.

Структуру передают в функцию указателем: передача по значению скопирует её целиком (у Student — 64 байта), а изменить исходную не сможет. Поле через указатель — операция ->: p->score — то же, что (*p).score.

/* Вывести запись; const — функция её не меняет */
void print_student(const Student *s)
{
    printf("%-12s %-6s %5.2f\n", s->name, s->group, s->score);
}

/* По убыванию балла, вставками — устойчиво: при равном балле порядок сохраняется */
void sort_by_score(Student a[], int n)
{
    for (int i = 1; i < n; i++) {
        Student current = a[i];
        int j = i - 1;
        while (j >= 0 && a[j].score < current.score) {
            a[j + 1] = a[j];
            j--;
        }
        a[j + 1] = current;
    }
}

/* Индекс студента с фамилией name или -1 */
int find_student(const Student a[], int n, const char *name)
{
    for (int i = 0; i < n; i++) {
        if (strcmp(a[i].name, name) == 0) {
            return i;
        }
    }
    return -1;
}

Обмен записей при сортировке копирует их целиком; для больших записей и больших массивов быстрее сортировать массив указателей или индексов, а записи не трогать. В разделе 15 эта же сортировка будет одним вызовом qsort с функцией сравнения.

03

Открытие и закрытие файла

Данные в переменных пропадают при завершении программы. Чтобы сохранить их, пишут в файл. Файл открывают функцией fopen, она возвращает указатель FILE * — через него идёт вся работа — или NULL, если открыть не удалось:

FILE *f = fopen("students.txt", "r");
if (f == NULL) {
    perror("students.txt");     /* students.txt: No such file or directory */
    return 1;
}
…
fclose(f);
РежимЧто делаетЕсли файла нет
"r"чтениеошибка, NULL
"w"запись; существующий файл стираетсясоздаётся
"a"дописывание в конецсоздаётся
"rb", "wb"то же в двоичном режимераздел 14
04

Запись и чтение текстового файла

Запись — как вывод на экран, только с первым аргументом-файлом: fprintf(f, …). printf(…) — это fprintf(stdout, …); сообщения об ошибках пишут в stderr: fprintf(stderr, "Error: …\n") — их видно, даже если вывод программы перенаправлен в файл.

Читать надёжнее всего построчно: fgets читает строку, а разбирает её sscanf. Так каждая строка файла обрабатывается отдельно, и ошибочная строка не сбивает чтение остальных. fgets возвращает NULL в конце файла — это и есть условие цикла:

/* Числа из файла, по одному в строке: количество и сумма */
char line[128];
int line_number = 0, count = 0;
long long sum = 0;
while (fgets(line, sizeof line, f) != NULL) {
    line_number++;
    int value = 0;
    char extra = 0;
    if (sscanf(line, "%d %c", &value, &extra) != 1) {
        fprintf(stderr, "Line %d: integer expected\n", line_number);
        continue;
    }
    sum += value;
    count++;
}

"%d %c" с проверкой на результат 1 — приём, чтобы отвергнуть строку вроде 12abc: если после числа что-то есть, %c это прочтёт и sscanf вернёт 2.

while (!feof(f)) — ошибка. feof становится истинным только после неудачной попытки чтения, а не когда данные кончились. Цикл while (!feof(f)) { fscanf(f, "%d", &x); sum += x; } на файле 10 20 30 выполнит тело четыре раза: последний fscanf ничего не прочтёт, x останется 30, и сумма будет 90 вместо 60. Условием цикла служит результат самой функции чтения: while (fgets(…) != NULL), while (fscanf(…) == 1).
05

Записи в файле: строки с разделителями

Запись хранят строкой, поля разделяют символом, которого нет в данных, — точкой с запятой (или запятой — формат CSV, его открывает Excel):

Ivanov;21V;4.75
Petrova;21V;3.80
Sidorov;22V;4.50

Разбор такой строки — sscanf с набором %[^;]: «читать всё, кроме точки с запятой». Ширина обязательна, как у %s:

/* Разобрать "фамилия;группа;балл" в *s; false — неверная строка */
bool parse_student(const char *line, Student *s)
{
    char extra = 0;
    int fields = sscanf(line, "%40[^;];%10[^;];%lf %c", s->name, s->group, &s->score, &extra);
    return fields == 3 && s->score >= 2 && s->score <= 5;
}

%40[^;] — до 40 символов, кроме ;, в массив на 41 байт. &s->score — адрес поля в структуре, на которую указывает s. Пробел перед %c пропускает перевод строки, оставленный fgets, поэтому extra ловит только настоящий мусор в конце. Запись в файл — обратное: fprintf(f, "%s;%s;%.2f\n", s->name, s->group, s->score).

06

Программа: файл → массив структур → отчёт и новый файл

Имя файла программа получает аргументом командной строки: students students.txt. Для этого у main другой заголовок: argc — число аргументов вместе с именем программы, argv — массив строк-аргументов.

/* Загрузить студентов, вывести по убыванию балла и записать отличников в honors.txt */
int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s students.txt\n", argv[0]);
        return 1;
    }
    FILE *in = fopen(argv[1], "r");
    if (in == NULL) {
        perror(argv[1]);
        return 1;
    }
    Student students[MAX_STUDENTS];
    int n = 0, line_number = 0;
    char line[128];
    while (fgets(line, sizeof line, in) != NULL) {
        line_number++;
        if (n == MAX_STUDENTS) {
            fprintf(stderr, "Too many lines, the rest is skipped\n");
            break;
        }
        if (parse_student(line, &students[n])) {
            n++;
        } else {
            fprintf(stderr, "Line %d skipped: %s", line_number, line);
        }
    }
    fclose(in);

    sort_by_score(students, n);
    FILE *out = fopen("honors.txt", "w");
    if (out == NULL) {
        perror("honors.txt");
        return 1;
    }
    for (int i = 0; i < n; i++) {
        print_student(&students[i]);
        if (students[i].score >= 4.5) {
            fprintf(out, "%s;%s;%.2f\n", students[i].name, students[i].group, students[i].score);
        }
    }
    fclose(out);
    return 0;
}
students.txt:                    вывод:                           honors.txt:
Ivanov;21V;4.75                  Kuznetsova   22V     5.00         Kuznetsova;22V;5.00
Petrova;21V;3.80                 Ivanov       21V     4.75         Ivanov;21V;4.75
Sidorov;22V;4.50                 Sidorov      22V     4.50         Sidorov;22V;4.50
Kuznetsova;22V;5.00              Petrova      21V     3.80
Orlov;21V;6.1                    (в stderr: Line 5 skipped: Orlov;21V;6.1)

Запуск — в консоли из папки, где лежит файл: students students.txt (в MSYS2 и Linux — ./students students.txt). В IDE аргументы задаются в настройках запуска. Ошибочная строка не останавливает загрузку: программа сообщает её номер и текст и продолжает — так ведут себя программы, которые обрабатывают реальные данные.

Ключевые выводы раздела

Запомните главное

  • Структура объединяет поля разных типов; поле — s.field, через указатель — p->field
  • Структуры присваиваются целиком, но не сравниваются ==; в функцию передаются указателем, только для чтения — const
  • fopen может вернуть NULL — проверять и сообщать perror; каждый файл закрывается fclose
  • "w" стирает существующий файл; относительный путь — от текущей папки запуска
  • Текстовый файл читают построчно fgets и разбирают sscanf; условие цикла — результат чтения, не feof
  • Поля в строке разделяют ;, разбор — %40[^;] с шириной
  • Ошибочная строка — сообщение с номером в stderr и продолжение
  • Имя файла — аргумент командной строки: main(int argc, char *argv[])
Связи раздела. Опирается на раздел 8 — массивы, поиск, сортировка вставками; раздел 10 — указатели и NULL; раздел 11 — строки, fgets, sscanf, snprintf. Нужен для: раздела 14 — двоичные файлы структур и прямой доступ; раздела 15 — qsort по полю структуры; раздела 17 — узлы списка как структуры; раздела 21 — классы как развитие структур.
Практическая работа №11 и тест по разделу 13 — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 12: Битовые операции и множества Все разделы Экзамен по блоку 1