Записи из нескольких полей, массивы записей и их хранение между запусками программы
->Студент — это фамилия, группа и средний балл: значения разных типов, которые описывают один объект. Массив их не объединит (в нём элементы одного типа). Объединяет структура (в 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; /* структуры присваиваются целиком */
typedef struct { … } Student; вводит имя типа Student. Без typedef тип называется struct student и пишется с struct каждый раз.s.score. У каждого поля свой тип; строки — массивы char фиксированного размера.== — сравнивают поля.struct { int year, month, day; } внутри записи студента, обращение s.birth.year.snprintf(s.name, sizeof s.name, "%s", source) (раздел 11). sizeof s.name — размер массива-поля, 41, его можно вычислить там, где видна структура.Группа студентов — массив структур: 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 с функцией сравнения.
Данные в переменных пропадают при завершении программы. Чтобы сохранить их, пишут в файл. Файл открывают функцией 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 |
perror(текст) выводит текст и причину последней ошибки системы: нет файла, нет прав, путь не существует."students.txt") отсчитывается от текущей папки — той, из которой запущена программа, а не той, где лежит exe. В IDE текущей папкой часто оказывается папка сборки — отсюда «файл есть, а программа его не видит».fclose: при записи данные копятся в буфере и попадают на диск при закрытии. Программа, которая не закрыла файл записи и аварийно завершилась, может оставить его пустым.'\n' записывается как два байта \r\n и при чтении превращается обратно; в Linux — один байт. Поэтому размер текстового файла в Windows больше на число строк.Запись — как вывод на экран, только с первым аргументом-файлом: 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).Запись хранят строкой, поля разделяют символом, которого нет в данных, — точкой с запятой (или запятой — формат 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).
Имя файла программа получает аргументом командной строки: 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==; в функцию передаются указателем, только для чтения — constfopen может вернуть NULL — проверять и сообщать perror; каждый файл закрывается fclose"w" стирает существующий файл; относительный путь — от текущей папки запускаfgets и разбирают sscanf; условие цикла — результат чтения, не feof;, разбор — %40[^;] с ширинойstderr и продолжениеmain(int argc, char *argv[])NULL; раздел 11 — строки, fgets, sscanf, snprintf. Нужен для: раздела 14 — двоичные файлы структур и прямой доступ; раздела 15 — qsort по полю структуры; раздела 17 — узлы списка как структуры; раздела 21 — классы как развитие структур.