Правильный способ чтения текстового файла в буфер на C?

Я имею дело с небольшими текстовыми файлами, которые я хочу прочитать в буфер, пока обрабатываю их, поэтому я придумал следующий код:

...
char source[1000000];

FILE *fp = fopen("TheFile.txt", "r");
if(fp != NULL)
{
    while((symbol = getc(fp)) != EOF)
    {
        strcat(source, &symbol);
    }
    fclose(fp);
}
...

Это правильный способ поместить содержимое файла в буфер, или я злоупотребляю strcat()?

Затем я перебираю буфер таким образом:

for(int x = 0; (c = source[x]) != '\0'; x++)
{
    //Process chars
}

person Gary Willoughby    schedule 08.01.2010    source источник
comment
Это не правильно. strcat объединяет строки. Даже если &symbol является char *, он не заканчивается нулем. Вы должны использовать fgets или fread. Кроме того, strcat в любом случае будет медленным в вашем случае, потому что он сканирует source каждый раз, когда ему нужно добавить один символ.   -  person Alok Singhal    schedule 08.01.2010
comment
Не говоря уже о том, что чтение символов за раз будет намного медленнее, чем при использовании fread.   -  person Nick Meyer    schedule 08.01.2010
comment
@Nick: Я не знаю насчет «намного медленнее»: из-за буферизации io и, возможно, встраивания вызовов функций, влияние на производительность не обязательно должно быть таким большим; использование fread() по-прежнему хорошая идея, хотя   -  person Christoph    schedule 08.01.2010
comment
кстати: я бы не стал считать 1-метровый текстовый файл маленьким;)   -  person Christoph    schedule 08.01.2010
comment
Посмотрите в mmap (), чтобы отобразить файл в памяти. Следите за переполнением буфера. Не используйте strcat () - даже если вы исправите проблемы с нулевым завершением, это даст вам квадратичное поведение, которое плохо для мегабайтных файлов и катастрофическое для гигабайтных файлов.   -  person Jonathan Leffler    schedule 08.01.2010
comment
@Alok: &symbol завершается нулем, если symbol является int, и вы используете архитектуру с прямым порядком байтов. Но не то, на что я бы хотел положиться.   -  person Mark Ransom    schedule 08.01.2010
comment
@Mark: а что, если sizeof(int) == 1? Как вы сказали, на это лучше не полагаться.   -  person Alok Singhal    schedule 08.01.2010


Ответы (8)


char source[1000000];

FILE *fp = fopen("TheFile.txt", "r");
if(fp != NULL)
{
    while((symbol = getc(fp)) != EOF)
    {
        strcat(source, &symbol);
    }
    fclose(fp);
}

В этом коде есть несколько ошибок:

  1. Это очень медленно (вы извлекаете буфер по одному символу за раз).
  2. Если размер файла превышает sizeof(source), это может привести к переполнению буфера.
  3. На самом деле, если присмотреться, этот код вообще не должен работать. Как указано на страницах руководства:

Функция strcat() добавляет копию строки с нулевым символом в конце s2 в конец строки с нулевым символом в конце s1, а затем добавляет завершающий `\ 0 '.

Вы добавляете символ (не строку с завершающим NUL!) К строке, которая может или не может быть завершена NUL. Единственное время, когда я могу представить эту работу в соответствии с описанием страницы руководства, - это если каждый символ в файле оканчивается NUL, и в этом случае это было бы бессмысленно. Так что да, это определенно ужасное злоупотребление strcat().

Ниже приведены две альтернативы, которые можно использовать вместо этого.

Если вы заранее знаете максимальный размер буфера:

#include <stdio.h>
#define MAXBUFLEN 1000000

char source[MAXBUFLEN + 1];
FILE *fp = fopen("foo.txt", "r");
if (fp != NULL) {
    size_t newLen = fread(source, sizeof(char), MAXBUFLEN, fp);
    if ( ferror( fp ) != 0 ) {
        fputs("Error reading file", stderr);
    } else {
        source[newLen++] = '\0'; /* Just to be safe. */
    }

    fclose(fp);
}

Или, если вы этого не сделаете:

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

char *source = NULL;
FILE *fp = fopen("foo.txt", "r");
if (fp != NULL) {
    /* Go to the end of the file. */
    if (fseek(fp, 0L, SEEK_END) == 0) {
        /* Get the size of the file. */
        long bufsize = ftell(fp);
        if (bufsize == -1) { /* Error */ }

        /* Allocate our buffer to that size. */
        source = malloc(sizeof(char) * (bufsize + 1));

        /* Go back to the start of the file. */
        if (fseek(fp, 0L, SEEK_SET) != 0) { /* Error */ }

        /* Read the entire file into memory. */
        size_t newLen = fread(source, sizeof(char), bufsize, fp);
        if ( ferror( fp ) != 0 ) {
            fputs("Error reading file", stderr);
        } else {
            source[newLen++] = '\0'; /* Just to be safe. */
        }
    }
    fclose(fp);
}

free(source); /* Don't forget to call free() later! */
person Michael    schedule 08.01.2010
comment
Вы, вероятно, захотите также завершить свой буфер нулем. Во втором примере кода вы оставили место для нуля, но на самом деле не установили его; в первом вы не оставили места для нуля. - person Brian Campbell; 08.01.2010
comment
@Brian: Это правда, я обновил примеры с учетом этого. - person Michael; 08.01.2010
comment
+1 за использование ftell и malloc. Это путь. - person cigarman; 08.01.2010
comment
Даже если каждый символ в файле завершен NUL, это не сработает. Вопрос в том, заканчивается ли указатель &symbol 0. - person Alok Singhal; 08.01.2010
comment
@Alok: Да, но символ & указывает на каждый символ в файле (по одному) ... поэтому, если символ & указывает на символ '\ 0', strcat остановится; в противном случае он будет продолжать работу, пока не найдет '\ 0' в каком-то случайном месте в памяти (символ & + x). Но теперь, когда я думаю об этом, вы правы в том, что на самом деле это никогда не должно работать, потому что источник никогда не инициализируется. - person Michael; 08.01.2010
comment
@Michael: getch возвращает int, предположительно, symbol - это локальная переменная, которая также является int. Пока знак не расширяется до отрицательного числа и не является EOF, он будет меньше 256. В архитектуре с прямым порядком байтов, такой как x86, это всегда будет завершаться нулем, так как старшие байты следуют за младший байт. Случайно, конечно. - person Mark Ransom; 08.01.2010
comment
Кроме того, некоторые компиляторы заполняют неинициализированные массивы нулями. Должно быть, ОП действительно повезло. - person Mark Ransom; 08.01.2010
comment
@Mark: ты прав, и я уверен, что ты это знаешь, но sizeof(int) может быть 1. @ Майкл, скажем, я прочитал букву "A" в symbol. Тогда &symbol (даже если symbol равно char) - это указатель, указывающий на 'A', за которым следуют случайные данные. Если symbol - это int и sizeof(int) > 1, то &symbol при преобразовании в char * указывает на 'A', за которым следует 0 или 0, в зависимости от порядка байтов машины. - person Alok Singhal; 08.01.2010
comment
@Alok, Марк: я забыл, что getc () возвращает int, а не char. Вау, это очень тонко. - person Michael; 09.01.2010
comment
stackoverflow.com/a/238607/309483: если вы используете ftell, вы должны открыть файл в двоичном режиме. Если вы откроете его в текстовом режиме, ftell вернет только файл cookie, который может использовать только fseek. - person Janus Troelsen; 19.10.2012
comment
Я думаю, что ваша проверка ошибок для fseek выполняется в обратном порядке. Должно быть if (fseek(...) != 0) {...error...} (первое использование правильное, а второе - наоборот) cplusplus.com/ ссылка / cstdio / fseek - person crazy2be; 02.11.2013
comment
@Michael - Использование calloc вместо malloc означало бы, что вам не нужно было бы ставить source[++newLen] = '\0'; - person Joseph; 21.02.2014
comment
Обратите внимание, что fseek() не работает на трубах или терминалах (или некоторых других устройствах, но вы вряд ли столкнетесь с ними). Таким образом, для этих типов файлов последний фрагмент кода не будет работать. Возможно написать код, который постепенно выделяет массив по мере чтения данных. Удваивайте размер буфера каждый раз, когда вам нужно больше места; вы можете освободить излишки в конце, если вас это беспокоит (возможно, вы читаете 1,1 MiB в буфер 2,0 MiB и хотите вернуть неиспользованные 0,9 MiB - это разумно). - person Jonathan Leffler; 09.04.2015
comment
Я предлагаю поставить пробел после sizeof, поскольку это оператор, а не вызов функции: sizeof (char). - person JohnMudd; 14.07.2018

Да, вы, вероятно, были бы арестованы за ужасное злоупотребление strcat!

Взгляните на getline (), он читает данные построчно, но, что важно, может ограничивать количество читаемых символов, чтобы вы не переполняли буфер.

Strcat относительно медленный, потому что он должен искать конец всей строки при каждой вставке символа. Обычно вы сохраняете указатель на текущий конец хранилища строк и передаете его в getline в качестве позиции для чтения следующей строки.

person Martin Beckett    schedule 08.01.2010

Если вы работаете в системе Linux, как только у вас есть дескриптор файла, вы можете получить много информации о файле с помощью fstat ().

http://linux.die.net/man/2/stat

так что у вас может быть

#include  <unistd.h> 
void main()
{
    struct stat stat;
    int fd;
    //get file descriptor
    fstat(fd, &stat);
    //the size of the file is now in stat.st_size
}

This avoids seeking to the beginning and end of the file.

person toweleeele    schedule 24.07.2015

См. эту статью JoelOnSoftware, чтобы узнать, почему вы не хотите использовать strcat.

Посмотрите на fread альтернативу. Используйте его с 1 для размера, когда вы читаете байты или символы.

person Mark Ransom    schedule 08.01.2010

Почему бы вам просто не использовать имеющийся у вас массив символов? Это должно сделать это:

   source[i] = getc(fp); 
   i++;
person Martin Wickman    schedule 08.01.2010

Не тестировался, но должен работать .. И да, это можно было бы лучше реализовать с помощью fread, я оставлю это в качестве упражнения для читателя.

#define DEFAULT_SIZE 100
#define STEP_SIZE 100

char *buffer[DEFAULT_SIZE];
size_t buffer_sz=DEFAULT_SIZE;
size_t i=0;
while(!feof(fp)){
  buffer[i]=fgetc(fp);
  i++;
  if(i>=buffer_sz){
    buffer_sz+=STEP_SIZE;
    void *tmp=buffer;
    buffer=realloc(buffer,buffer_sz);
    if(buffer==null){ free(tmp); exit(1);} //ensure we don't have a memory leak
  }
}
buffer[i]=0;
person Earlz    schedule 08.01.2010
comment
не будет realloc медленным? - person ajay; 13.01.2014
comment
Sorta, но вам действительно нужно беспокоиться о char *buffer[DEFAULT_SIZE], потому что это массив указателей, а не символов. Присвоение buffer[i] в лучшем случае сомнительно; fgetc() возвращает char, а не char *. Если мы сделаем вид, что это char *buffer = 0;, вы почти у цели. Вам нужно прочитать символ в int и сохранить его в массиве только в том случае, если вы уверены, что это не EOF и достаточно места. while (!feof(file)) всегда неверно! Этот ответ требует значительной работы (но это основа для того, что может быть хорошим ответом). - person Jonathan Leffler; 09.04.2015


Вы рассматривали mmap ()? Вы можете читать из файла напрямую, как если бы он уже был в памяти.

http://beej.us/guide/bgipc/output/html/multipage/mmap.html

person Ioan    schedule 08.01.2010