Использование fread() для чтения текстового файла — лучшие практики

Рассмотрим этот код для чтения текстового файла. Этот вид использования fread() был кратко затронут в отличной книге C Programming: A Modern Approach автора К.Н. Король. Есть и другие способы чтения текстовых файлов, но здесь меня интересует только fread().

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    // Declare file stream pointer.
    FILE *fp = fopen("Note.txt", "r");
    // fopen() call successful.
    if(fp != NULL)
    {
        // Navigate through to end of the file.
        fseek(fp, 0, SEEK_END);
        // Calculate the total bytes navigated.
        long filesize = ftell(fp);
        // Navigate to the beginning of the file so
        // it can be read.
        rewind(fp);
        // Declare array of char with appropriate size.
        char content[filesize + 1];
        // Set last char of array to contain NULL char.
        content[filesize] = '\0';
        // Read the file content.
        fread(content, filesize, 1, fp);
        // Close file stream pointer.
        fclose(fp);
        // Print file content.
        printf("%s\n", content);
    }
    // fopen() call unsuccessful.
    else
    {
        printf("File could not be read.\n");
    }
    return 0;
}

У меня есть некоторые проблемы с этим методом. Я считаю, что это небезопасный метод выполнения fread(), так как может произойти переполнение, если мы попытаемся прочитать очень большую строку. Правомерно ли это мнение?

Чтобы обойти эту проблему, мы можем использовать размер буфера и продолжать чтение в массив символов этого размера. Если размер файла меньше размера буфера, мы просто выполняем fread() один раз, как описано в приведенном выше коде. В противном случае мы делим общий размер файла на размер буфера и получаем результат, чью часть int мы будем использовать в качестве общего количества итераций цикла, где мы будем каждый раз вызывать fread(), добавляя массив буфера чтения в большую строку. . Теперь для финального fread(), который мы выполним после цикла, нам нужно будет прочитать точно (размер файла % размер буфера) байт данных в массив такого размера и, наконец, добавить этот массив в большую строку (что у нас будет malloc -ed с размером файла + 1 заранее). Я обнаружил, что если мы выполним fread() для последнего фрагмента данных, используя размер буфера в качестве второго параметра, то будут считаны дополнительные мусорные данные размера (размер буфера - размер фрагмента), и данные могут быть повреждены. Верны ли мои предположения? Пожалуйста, объясните, если / как я что-то упустил из виду.

Кроме того, существует проблема, что символы, отличные от ASCII, могут иметь размер не 1 байт. В этом случае я бы предположил, что читается правильное количество, но каждый байт читается как один символ, поэтому текст как-то искажается? Как fread() справляется с чтением многобайтовых символов?

Вызов ftell гарантирует только количество байтов от начала файла, когда файл открыт в двоичном режиме. Однако вы открываете файл в текстовом режиме. В этом режиме значение, возвращаемое ftell, имеет смысл только для fseek. Однако это, вероятно, не будет проблемой, потому что значение, которое вы получите для размера файла, будет скорее слишком большим, чем слишком низким.

Andreas Wenzel 23.04.2022 04:55

если есть многобайтовые символы, это действительно текстовый файл? Вроде, конечно, но fread не справляется с этим, и вы не можете очень хорошо использовать char, если хотите иметь многобайтовые символы.

Garr Godfrey 23.04.2022 05:04

@GarrGodfrey У меня сложилось впечатление, что char может обрабатывать многобайтовые символы, такие как unicode. В этом случае вместо 1 байта char представляет собой другую версию размером 2 байта. В Windows это будет wchar. Пожалуйста, поправьте меня, если я ошибаюсь.

mindoverflow 23.04.2022 05:11

@AndreasWenzel, не могли бы вы рассказать об этом подробнее? Почему возвращаемое значение ftell будет иметь смысл только для fseek, если мы читаем файл в текстовом режиме? ftell дает мне правильный размер моего текстового файла, который я тестировал, хотя и ограниченно.

mindoverflow 23.04.2022 05:13

Неспособность проверить возврат от fread() и сравнить с filesize — это способ № 1 попасть в беду. При кратком чтении не забудьте проверить feof() и ferror(), чтобы определить, почему произошел сбой. Вы можете использовать stat(), чтобы получить количество байтов в файле независимо от типа содержимого. Затем двоичное чтение в content гарантирует, что вы прочитаете весь файл. Трюк с filesize + 1 просто позволяет рассматривать contents как строку. Пока ваш файл содержит однобайтовые символы, это будет работать. Обратите также внимание, что char content[filesize + 1]; — это VLA.

David C. Rankin 23.04.2022 05:19

@mindoverflow: В Microsoft Windows, например, в текстовом режиме окончания строк \r\n преобразуются в \n, т.е. 2 байта сокращаются до 1 байта. Это означает, что количество байтов, доступных для чтения в текстовом режиме, не будет равно фактическому размеру файла, сообщаемому операционной системой. Стандарт ISO C допускает такое поведение, поэтому он указывает, что значение, возвращаемое ftell, не указано и имеет смысл только для fseek, когда файл открыт в текстовом режиме.

Andreas Wenzel 23.04.2022 05:23

@DavidC.Rankin, спасибо, что обратили на это мое внимание. странно, что ни пример из учебника, ни онлайн-примеры этого не делают. с какой ошибкой я могу столкнуться, если я не проверю? с другой стороны, довольно интересно, что ведутся дебаты о том, почему feof() не возвращает фактический EOF, и что это просто флаг, который активируется в определенных обстоятельствах. Я хотел бы услышать ваше и другое профессиональное мнение об этом когда-нибудь.

mindoverflow 23.04.2022 05:25

@DavidC.Rankin: Функция stat не существует в ISO C. Эта функция является расширением POSIX.

Andreas Wenzel 23.04.2022 05:26

@AndreasWenzel, это довольно интересно, а также немного беспокоит, потому что есть ли способ получить фактический размер файла с помощью c?

mindoverflow 23.04.2022 05:27

@AndreasWenzel, по вашему мнению, что было бы идеальным и надежным способом чтения текстового файла в c. я не против POSIX, так как он уже довольно широко распространен. не стесняйтесь отвечать, поскольку вы ответили на все мои вопросы.

mindoverflow 23.04.2022 05:30

@AndreasWenzel - хороший улов, переполнение разума, см. человек 3, при кратком чтении fread() не различает, что произошло, поэтому вы должны вызвать feof() и ferror(), чтобы определить, что именно. Существует ряд причин, по которым ошибка «может» возникнуть: ошибка потока, ошибка диска, повреждение файла и т. д. Я не запомнил обе стороны EOF дебатов, но, насколько я помню, feof() проверю, EOF индикатор потока установлен. Проблема в том, что в некоторых случаях индикатор могут устанавливать ошибки, отличные от конца файла (хотя я не помню, что и когда)

David C. Rankin 23.04.2022 05:34

@DavidC.Rankin отличный вклад. теперь я буду ссылаться на справочную страницу. Я обычно проверяю пару источников для функций c, но никогда не думал проверять справочные страницы Linux, так как я обычно основан на Windows. но я полагаю, что лучше всего придерживаться системных стандартов на основе Unix для согласованности в этих вопросах.

mindoverflow 23.04.2022 05:38

@mindoverflow: в POSIX нет разницы между текстовым и двоичным режимами. В отличие от Microsoft Windows, POSIX использует окончания строк \n как в текстовом, так и в двоичном режиме. Поэтому все проблемы и ограничения, существующие при использовании текстового режима, отсутствуют на платформах POSIX. По этой причине ваш способ определения размера файла будет работать на платформах POSIX. Все проблемы, которые я упомянул, относятся к ISO C в целом, что важно только в том случае, если вы хотите, чтобы ваш код был переносимым на другие платформы.

Andreas Wenzel 23.04.2022 06:01

@mindoverflow: Вы можете прочитать этот вопрос: Как определить размер файла в C? Большинство ответов относятся к POSIX, но один также относится к Windows. Также упоминается ваш способ определения размера файла.

Andreas Wenzel 23.04.2022 06:02

@mindoverflow: связанный вопрос в моем предыдущем комментарии использует слова «размер файла» в смысле длины файла в двоичном режиме (это размер, который обычно сообщает операционная система). Однако в ISO C единственным способом определить размер файла в текстовом режиме является чтение всего файла в текстовом режиме и подсчет количества прочитанных байтов.

Andreas Wenzel 23.04.2022 06:14

Я понимаю. Спасибо за информацию. Причина, по которой я не сталкиваюсь с какими-либо проблемами, заключается в том, что я использую POSIX-версию mingw64 на своей машине. Я заранее решил это, потому что знал, что Windows каким-то образом нарушает установленные стандарты и настраивает свои собственные. классический пример \ вместо / для путей

mindoverflow 23.04.2022 06:22

Незначительное: printf("File could not be read.\n"); не соответствует описанию. Лучше как printf("File could not be opened.\n");

chux - Reinstate Monica 23.04.2022 09:54

@chux-ReinstateMonica отличный и важный момент. благодарю вас.

mindoverflow 30.04.2022 05:23
3 метода стилизации элементов HTML
3 метода стилизации элементов HTML
Когда дело доходит до применения какого-либо стиля к нашему HTML, существует три подхода: встроенный, внутренний и внешний. Предпочтительным обычно...
Формы c голосовым вводом в React с помощью Speechly
Формы c голосовым вводом в React с помощью Speechly
Пытались ли вы когда-нибудь заполнить веб-форму в области электронной коммерции, которая требует много кликов и выбора? Вас попросят заполнить дату,...
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Будучи разработчиком веб-приложений, легко впасть в заблуждение, считая, что приложение без JavaScript не имеет права на жизнь. Нам становится удобно...
Flatpickr: простой модуль календаря для вашего приложения на React
Flatpickr: простой модуль календаря для вашего приложения на React
Если вы ищете пакет для быстрой интеграции календаря с выбором даты в ваше приложения, то библиотека Flatpickr отлично справится с этой задачей....
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Клиент для URL-адресов, cURL, позволяет взаимодействовать с множеством различных серверов по множеству различных протоколов с синтаксисом URL.
1
18
67
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

this is not a safe method of performing fread() since there might be an overflow if we try to read an extremely large string. Is this opinion valid?

fread() не заботится о струны (массивы с завершением нулевой символ). Он считывает данные, как если бы они были кратны unsigned char*1, не обращая особого внимания на содержимое данных, если поток открыт в режиме бинарный и, возможно, некоторую обработку данных (например, конец строки, метку порядка байтов) в режиме текст.

Are my assumptions here correct?

Неудачные предположения:

  • Предположим, что возвращаемое значение ftell() равно сумме fread() байтов. Предположение может быть ложным в режиме текст (поскольку OP открыл файл) и fseek() до конца является техническим неопределенное поведение в режиме бинарный.

  • Предполагая, что не проверять возвращаемое значение fread(), все в порядке. Используйте возвращаемое значение fread(), чтобы узнать, произошла ли ошибка, конец файла и сколько кратных байтов было прочитано.

  • Предполагая, что проверка ошибок не требуется. , ftell(), fread(), fseek() вместо rewind() все заслуживают проверки на ошибки. В частности, ftell() легко терпит неудачу на потоки, у которых нет определенного конца.

  • Предполагая, что нулевые символы не читаются. Текстовый файл, безусловно, не превращается в один нить путем чтения всего и добавления нулевой символ. Надежный код обнаруживает и/или справляется со встроенными нулевыми символами.

  • Многобайтовый: при условии, что ввод соответствует требованиям кодирования. Пример: надежный код обнаруживает (и отклоняет) недопустимые последовательности UTF8 — возможно, после чтения всего файла.

  • Экстрим: Предполагая файл length <= LONG_MAX, максимальное значение возвращается из ftell(). Файлы могут быть больше.

but each byte is being read as a single char, so the text is distorted somehow? How is fread() handling reading of multi-byte chars?

fread() не работает с многобайтовыми границами, только с кратными unsigned char. Данный fread() может заканчиваться частью мультибайта, а следующий fread() будет продолжаться с середины мультибайта.


Вместо 2-проходного подхода рассмотрите 1 одиночный проход

// Pseudo code
total_read = 0      
Allocate buffer, say 4096

forever
  if buffer full
    double buffer_size (`realloc()`)
  u = unused portion of buffer 
  fread u bytes into unused portion of buffer
  total_read += number_just_read
  if (number_just_read < u) 
    quit loop

Resize buffer total_read (+ 1 if appending a '\0')

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


Передовой

Текстовые файлы может быть только простым ASCII, определенным 8-битным кодовая страница, одной из различных кодировок UTF (метка порядка байтов и т. д. Последний линия может заканчиваться или не заканчиваться на '\n'. Надежная обработка текста помимо простого ASCII нетривиальна.

ASCII и UTF-8 являются наиболее распространенными. ИМО, обработайте 1 или оба из них и ошибитесь во всем, что не соответствует их требованиям.


*1fread() считывает несколько байтов в соответствии с третьим аргументом, который равен 1 в случае OP.

//                       v --- multiple of 1 byte
fread(content, filesize, 1, fp);

Стоит отметить, что в простом ISO C нет решения проблемы length <= LONG_MAX. Для решения этой проблемы требуется функциональность, зависящая от платформы, например, функция _ftell64 в Microsoft Windows. В 64-битном Linux это не проблема, как sizeof(long) == 8, тогда как в Microsoft Windows это проблема, как sizeof(long) == 4.

Andreas Wenzel 24.04.2022 18:25

@AndreasWenzel «нет решения проблемы длины <= LONG_MAX» --> Согласен. Как было предложено выше, не используйте ftell() или подобные альтернативы для определения размера файла, а продолжайте чтение до конца файла.

chux - Reinstate Monica 24.04.2022 18:29

Спасибо за отличный ответ! У меня все еще была некоторая путаница в отношении переполнения. Я читал, что fread() не различает символы/кодировку/и т. д. Но каким бы ни был тип файла, если он слишком велик, не будет ли переполнения, если только мы не читаем поток в виде фрагментов, так что мы заменить тот же массив следующей порцией информации? если C автоматически не переходит в режим ядра и не использует виртуальную подкачку или какое-то «временное хранилище - затем обеспечить требуемый раздел по запросу»?

mindoverflow 30.04.2022 05:29

Другие вопросы по теме