---
tags: си, прога
---
# Чтение из файлов в си
Как читать файлы в C и обработать все ошибки?
## `fgetc`
```c
if (fgetc(f) == '#') {
while (fgetc(f) != '\n') {}
}
```
Вот тут мы скипаем по одному байту, пока не наткнёмся на `\n`. Его тоже съедим и поедем дальше.
Ошибки все игнорируются. Это... страшно. Какие они могут быть?
- `EOF` — страшная аббриевиатура означает `end of file`. `fgetc` возвращает не `char`, а `int`. Потому что она может вернуть и `EOF`. Это будет означать, что файл закончился.
- Другие ошибки чтения. Жёсткий диск умер внезапно, прочесть не смогли, система кидает ошибку, потоп, пожар, всё что ни есть на свете может разные ошибки заставить вернуть `fgetc`.
В общем, если какая-то ошибка есть, получим вместо символа `EOF`. Это надо проверить. Раздуваем код выше:
```c
int skip_comment_if_any(FILE *f) {
int ch = fgetc(f);
if (ch == EOF) {
return -1;
}
if (ch != '#') {
// коммента нет, всё ок
return 0;
}
while (1) {
ch = fgetc(f);
if (ch == EOF) {
// файл скончался преждевременно
return -1;
}
if (ch == '\n') {
// кончилась строка
return 0;
}
}
}
```
Вроде такого. Прочесть что-то из файла — полбеды. Другая половина — обработать все ошибки. В си вынуждены мы страдать.
Чтобы страдать меньше, всё растаскиваем по функциям. Скипать коммент — функция. Прочесть хедер в файле — функция. Раны (от слова run) из самих данных поля скушать — функция.
## `fgets`
Итак, это был `fgetc`. Заменим `c` на `s`: `fgets` кушает несколько байт из файла и сохраняет их в буфер. Отличие от `fread`: в конец пихается `\0`. Удобно.
```c
char *str = calloc(10, sizeof(char));
fgets(str, 10, f);
printf("%s", str);
```
Она прочтёт **девять** байт, а десятый будет `\0`. Или не десятый, если не хватит в файле байт.
Какие тут ошибки? Ну да всё те же самые. Если будет ошибка, `fgets` вернёт `NULL` для разнообразия.
```c
char *str = calloc(10, sizeof(char));
if (fgets(str, 10, f) == NULL) {
// ошибка чтения
return -1;
}
printf("%s", str);
```
Так-то лучше.
## `fscanf`
Есть ещё одна полезная функция для чтения из файла. Это уже formatted input/output называется. Для файлов `fscanf` нас интересует.
Это... вариант для ленивых, на самом деле. Как известно, `fscanf` делает всё на свете: и числа читает, и строки, и байты. За удобство платим, конечно. Ошибки обрабатывать сложно.
Смотрим сюда. В файле записано `test`.
```c
int count = 0;
fscanf(f, "%d", &count);
print(count);
```
Я без понятия, что этот код выведет, потому что в файле даже цифры нет. Это плохо.
Нам надо будет парсить RLE — это, если забыть про комменты и хедер, набор данных рода `<число><символ>`. Число — это количество символов `<символ>`, которые мы схлопнули здесь.
```
2ob4o
```
Это закодирована строка `ooboooo`. Вот мы начали писать функцию `read_run`.
```c
int read_run(FILE *f, /* какие-то ещё параметры */) {
int count = 0;
fscanf(f, "%d", &count);
// Про остальное пока забудем...
}
```
Мы ленивы, юзаем `fscanf`. Попробуем поломать нашу наивную функцию. Дадим ей `test`. Будет ошибка.
Узнать об ошибке мы можем, если посмотрим, что вернёт `fscanf`. Она плюётся числом — количеством **скушанных процентиков**. В коде выше с `test` вернётся `0`, то есть числа там не прочёл `fscanf`. Хорошо:
```c
int count = 0;
// В строке формата у нас один процент,
// так что fscanf должен вернуть 1.
if (fscanf(f, "%d", &count) != 1) {
// Сисла нет, дефолтимся в 1.
count = 1;
}
```
Почему `1`? Ну, единичный символ в RLE пишет без числа. Просто `o` кодирует просто `o`, без лишних заворотов. То есть один символ.
Замечательно. `fscanf` может ещё заменить `fgetc` волшебным заклинанием `%c`:
```c
// Продолжаем:
char ch;
if (fscanf(f, "%c", &ch) != 1) {
// Что-то символ не смогли прочесть.
// Похоже, что файл скончался.
return -1;
}
```
Наверное, это удобнее. Теперь мы прочли число и символ, можем обрабатывать его.
А все ли ошибки учли? Ноп.
Пусть в файле лежит `-2o`. Первый `fscanf`, который с `%d`, радостно сожрёт `-2`. Ну, отрицательный размер — это странно. Вернёмся к коду тому и добавим:
```c
int count = 0;
if (fscanf(f, "%d", &count) != 1) {
// Числа нет, дефолтимся в 1.
count = 1;
}
if (count < 0) {
// Отрицательное число — ошибка.
return -1;
}
```
Тяк, всё обработали?
- [x] Отрицательные числа
- [x] Положительные числа
- [x] Ноль
- [ ] Слишком большие числа
А, не, погодите. Пусть в файле лежит `123456789012345678901234567890`. В `int` такое число не влезет. Что вернёт `fscanf`? Ща погуглим.
...
Гугл что-то молчит, тогда проверим сами:
```c
#include <stdio.h>
int main() {
int number;
printf("Enter a number: ");
scanf("%d", &number);
printf("Your input: %d\n", number);
return 0;
}
```
Компилируем и запускаем:
```
$ gcc main.c -o ./prog
$ ./prog
Enter a number: 1234567898012345678901234567890
Your input: -1
```
Нехорошо, нехорошо...
Окей, короче, я всё это говорил к тому, что `fscanf` в жизни мы сможем юзать только для чтения одного байта, наверное. Потому что нельзя нормально обработать ошибки.
## Как прочесть число
Забудем про `fscanf` окончательно и будем сами красиво парсить числа.
Немного вводных данных. В `<string.h>` есть функция под именем [`strtoull`](https://en.cppreference.com/w/c/string/byte/strtoul). Она берёт строку и преобразует её в число. Пытается, как минимум. Если не может, то функция не взрывается. Значит, можно использовать.
```c
#include <stdint.h>
// Возвращаем int — это код ошибки.
// Сам результат будет в *result.
int read_count(FILE *f, unsigned long long *result) {
// 1. В unsigned long long влезет до 18446744073709551615. Это 20 цифр.
// Поэтому создаём буфер на 21 символ, которые мы будем читать.
// двадцать первый — это `\0`.
char str[21] = {'\0'};
size_t len = 0;
bool significant_digits_started = false;
// 2. Читаем по одной цифре, пока число не закончится.
for (int i = 0; i < 20; ++i) {
int ch = fgetc(f);
if (ch != EOF && !isdigit(ch)) {
// Мы сожрали один байт, и это не цифра.
// Вернём байт назад.
// ungetc также может завершиться с ошибкой...
if (ungetc(ch, f) == EOF) {
return -1;
}
}
if (ch == EOF || !isdigit(ch)) {
// Не цифра или конец файла: завершаем строку, брякаемся.
str[i] = '\0';
break;
}
// Вот этот иф будет скипать нули в начале числа.
if (ch != '0' || significant_digits_started) {
str[i] = (char) ch;
len++;
}
}
// 3. Вызываем strtoull
if (len == 0) {
// Если цифру не встретили, выходим.
return -2;
}
char *end;
*result = strtoull(str, &end, 10);
// end после вызова указывает на последний байт, который strtoull обработал.
// Точнее, на следующий за последним обработанным байт.
// Если он завершится посередине, не на конце строки, то он столкнулся с ошибкой.
if (end != &str[len]) {
// Ошибка!!!!!111111
return -3;
}
// 4. Ошибок нет. Число в *result.
return 0;
}
```
Фух. Сколько раз я мог здесь прострелить свою бедную ногу, боюсь считать. А я это ещё не дебажил. Не буду лишать веселья этим заниматься.
А, да, к слову. `ungetc` там есть — это значит взять символ и засунуть его обратно в файл. Если прочесть его позже, то мы его получим назад.
Такое можно сделать только один раз. Сразу 10 символов обратно не запихаем, к сожалению.
## Итог
Итак, в конце этого документа надо что-то заключительное рассказать.
Ну, мы узнали, что `fgetc` нужен, чтобы читать один байт.
`fgets` нужен, чтобы прочесть сразу несколько.
`fscanf` нужен почти никогда, потому что он игнорирует ошибки. Только для прототипов хороша функция эта.
Наконец, есть код, которым можно прочесть одно число из файла. И в нём узнали про `strtoull` и `ungetc`.
## Конец
(файла). Это как постскриптум.
Не всегда конец файла — это ошибка. Например, если данные закончились, то конец файла закономерен. Нужно тщательно подумать в коде о том, когда `EOF` ошибка, а когда нет.
По-моему, я писал выше где-то, что `EOF` на самом деле возвращается при всём подряд: конце файла, другой ошибке чтения.
Поэтому нужно знать ещё 2 функции для полного счастья.
- `feof(f)` — кончился ли файл.
- `ferror(f)` — встретилась ли какая-то другая ошибка.
Жонглируя этими функциями, мы можем правильно обработать ошибки.
```c
while (true) {
unsigned long long count = 0;
int status = read_count(file, &count);
if (status == -1) {
// EOF или ошибка чтения: и то, и другое для нас ошибка,
// потому что файлу рано кончаться здесь
return -1;
} else if (status == -2) {
// Не встретили цифру: будем считать, что дали 1
count = 1;
} else if (status == -3) {
// Слишком огромное число. Выходим.
return -1;
}
if (count == 0) {
// Дали `0o` какой-нибудь. Это невалидно. Длины больше нуля должны быть.
return -1;
}
int ch = fgetc(file);
if (ch == EOF) {
if (status == -2 && feof(file)) {
// Не встретили цифру, но дошли до EOF.
// Файл, то бишь, кончился.
// Значит, всё кончилось, выходим. Это не ошибка.
return 0;
}
// Иначе, значит, ошибка.
return -1;
}
// Теперь можно что-то делать с count и ch.
}
```
Вот заготовка для чтения данных поля. Чтобы понять, что файл кончился по-хорошему, используем `feof`.
Теперь точно всё.