--- 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`. Теперь точно всё.