首页 > 解决方案 > 需要帮助在 C 中逐个字符地读取文件

问题描述

我有一个关于逐个字符读取文件并在 C 中计数的问题

这是我的代码在下面

void read_in(char** quotes){
    FILE *frp = fopen(IN_FILE, "r");
    
    char c;
    size_t tmp_len =0, i=0;    
    //char* tmp[100];
    //char* quotes[MAX_QUOTES];
    //char str = fgets(str, sizeof(quotes),frp);
    
    while((c=fgetc(frp)) != EOF){        
        if(frp == NULL){
            printf("File is empty!");
            fclose(frp); exit(1);
        }
        
        else{  
            if(c != '\n'){           
                printf("%c",c);
                c=fgetc(frp);
                tmp_len++;                 
            }        
        
        }
        char* tmp = (char*)calloc(tmp_len+1, sizeof(char));        
        fgets(tmp, sizeof(tmp), frp);
        strcpy((char*)quotes[i], tmp);        
        
        printf("%s\n", (char*)quotes[i]);
        
        i++;
    }
}

它不起作用,但我不明白为什么。

谢谢

标签: cfunctioncharacterfunction-pointersdouble-pointer

解决方案


从您的问题和评论来看,您希望将文件中的所有引号(行)读入动态分配的存储(屏幕 1),然后按长度对行进行排序并输出前 5 条最短的行(屏幕 2)将 5 条最短的行保存到第二个输出文件(这部分留给您)。从文件中读取和存储所有行并不困难——但也不是微不足道的。这听起来很基本,而且确实如此,但它要求您正确使用与持久存储(从磁盘/存储介质读取文件)和计算机的内存子系统 (RAM) 接口所需的所有基本工具。

从文件中读取每一行并不难,但就像 C 中的任何内容一样,它需要您注意细节。您可以使用面向字符的输入函数(fgetc()getc()等)从文件中读取,可以使用格式化输入函数(fscanf()),也可以使用面向行的输入函数,例如(fgets()或 POSIX getline())。从文件中读取行通常是使用面向行的函数来完成的,但使用面向字符的方法也没有错。实际上,您可以相对轻松地编写一个基于该函数的函数,该函数fgetc()将为您从文件中读取每一行。

在您知道文件中最长行的最大字符数的简单情况下,您可以使用 2D 字符数组来存储整个文件。这通过消除动态分配存储空间的需要简化了该过程,但有许多缺点,例如文件中的每一行需要与文件中最长的行相同的存储空间,并且通过限制可以存储的文件大小到程序堆栈的大小。malloc使用 ( 、calloc或)动态分配存储可以realloc消除这些缺点和效率低下的问题,从而使您可以将文件存储到计算机上可用内存的限制。(这里有一些方法可以通过使用滑动窗口技术来处理任何大小的文件,这远远超出了您的需要)

处理动态分配的内存,或者逐个字符地在其中复制或存储数据并不困难。也就是说,每个分配的责任,跟踪写入每个分配块的数据量,重新分配以调整块大小以确保没有数据写入每个块的边界之外,然后在不再需要时释放每个分配的块 - - 是你的,程序员。C 赋予程序员使用可用内存的每个字节的权力,并赋予程序员正确使用内存的责任。

存储文件的基本方法很简单。您从文件中读取每一行,为每个字符分配/重新分配存储空间,直到遇到'\n'or为止EOF。为了协调所有行,您分配一个指针块,然后将每个保存行的内存块的地址分配给一个指针,依次重新分配保存所有行所需的指针数量。

有时候,一张照片真的值 1000 字。使用基本方法,您声明一个指针(指向什么?)一个指针,以便您可以分配一个包含指针的内存块,您将为每个分配的行分配指针。例如,您可以声明,指向指针char **lines;指针是指向包含指针的内存块的单个指针。然后每个指针的类型lineschar *指向每个包含文件行的块,例如

 char **lines;
    |
    |      allocated
    |      pointers    allocated blocks holding each line
  lines --> +----+     +-----+
            | p1 | --> | cat |
            +----+     +-----+--------------------------------------+
            | p2 | --> | Four score and seven years ago our fathers |
            +----+     +-------------+------------------------------+
            | p3 | --> | programming |
            +----+     +-------------------+
            | .. |     |        ...        |
            +----+     +-------------------+
            | pn | --> | last line read |
            +----+     +----------------+

您可以lines通过分配1个额外的指针并初始化该指针来使使用更加灵活,该指针允许您在不知道有多少行的情况下NULL进行迭代——直到遇到,例如linesNULL

            | .. |     |        ...        |
            +----+     +-------------------+
            | pn | --> | last line read |
            +----+     +----------------+
            |pn+1|     | NULL |
            +----+     +------+

虽然您可以将所有这些放在一个函数中,以帮助学习过程(并且只是为了实际可重用性),但将其分解为两个函数通常更容易。一个为每一行读取和分配存储,第二个函数基本上调用第一个函数,分配指针并将每个分配的内存块的地址分配给从文件中读取的行依次分配给下一个指针。完成后,您有一个已分配的指针块,其中每个指针都保存(指向)已分配块的地址,该块保存文件中的一行。

您已表明要从文件中fgetc()读取并一次读取一个字符。这并没有什么问题,而且这种方法几乎没有任何损失,因为底层 I/O 子系统提供了一个读取缓冲区,您实际上是从中读取而不是一次从磁盘读取一个字符。(大小因编译器而异,但一般通过BUFSIZ宏提供,Linux 和 Windows 编译器都提供这个)

实际上有无数种方法可以编写一个函数,该函数分配存储空间来保存一行,然后从文件中读取一行,一次一个字符,直到遇到 a '\n'or EOF。您可以返回指向保存该行的已分配块的指针并传递一个指针参数以使用该行中包含的字符数进行更新,或者您可以让函数返回行长度并将指针的地址作为在函数中分配和填充的参数。它是由你决定。一种方法是:

#define NSHORT 5        /* no. of shortest lines to display */
#define LINSZ 128       /* initial allocation size for each line */
...

/** read line from 'fp' stored in allocated block assinged to '*s' and
 *  return length of string stored on success, on EOF with no characters
 *  read, or on failure, return -1. Block of memory sized to accommodate
 *  exact length of string with nul-terminating char. unless -1 returned,
 *  *s guaranteed to contain nul-terminated string (empty-string allowed).
 *  caller responsible for freeing allocated memory.
 */
ssize_t fgetcline (char **s, FILE *fp)
{
    int c;                              /* char read from fp */
    size_t n = 0, size = LINSZ;         /* no. of chars and allocation size */
    void *tmp = realloc (NULL, size);   /* tmp pointer for realloc use */
    
    if (!tmp)       /* validate every allocation/reallocation */
        return -1;
    
    *s = tmp;       /* assign reallocated block to pointer */
    
    while ((c = fgetc(fp)) != '\n' && c != EOF) {   /* read chars until \n or EOF */
        if (n + 1 == size) {                        /* check if realloc required */
            /* realloc using temporary pointer */
            if (!(tmp = realloc (*s, size + LINSZ))) {
                free (*s);              /* on failure, free partial line */
                return -1;              /* return -1 */
            }
            *s = tmp;                   /* assign reallocated block to pointer */
            size += LINSZ;              /* update allocated size */
        }
        (*s)[n++] = c;                  /* assign char to index, increment */
    }
    (*s)[n] = 0;                        /* nul-terminate string */
    
    if (n == 0 && c == EOF) {   /* if nothing read and EOF, free mem return -1 */
        free (*s);
        return -1;
    }
    
    if ((tmp = realloc (*s, n + 1)))    /* final realloc to exact length */
        *s = tmp;                       /* assign reallocated block to pointer */
    
    return (ssize_t)n;      /* return length (excluding nul-terminating char) */
}

注意:ssize_t是一个有符号类型,提供的范围size_t基本上允许返回-1。它在sys/types.h标题中提供。您可以根据需要调整类型)

fgetclines()函数进行最后一次调用,realloc以将分配的大小缩小到容纳行和nul 终止字符所需的确切字符数。

fgetclines()在根据需要分配和重新分配指针的同时读取文件中所有行的函数与上述函数对字符的作用基本相同。它只是分配一些初始数量的指针,然后开始从文件中读取行,每次需要时重新分配两倍数量的指针。它还添加了一个额外的指针NULL作为标记,允许迭代所有指针直到NULL到达(这是可选的)。该参数n将更新为存储的行数,以使其在调用函数中可用。这个函数也可以用多种不同的方式编写,一种是:

/** read each line from `fp` and store in allocated block returning pointer to
 *  allocateted block of pointers to each stored line with the final pointer
 *  after the last stored string set to NULL as a sentinel. 'n' is updated to
 *  the number of allocated and stored lines (excluding the sentinel NULL).
 *  returns valid pointer on success, NULL otherwise. caller is responsible for
 *  freeing both allocated lines and pointers.
 */
char **readfile (FILE *fp, size_t *n)
{
    size_t nptrs = LINSZ;               /* no. of allocated pointers */
    char **lines = malloc (nptrs * sizeof *lines);  /* allocated bock of pointers */
    void *tmp = NULL;                   /* temp pointer for realloc use */
    
    /* read each line from 'fp' into allocated block, assign to next pointer */
    while (fgetcline (&lines[*n], fp) != -1) {
        lines[++(*n)] = NULL;           /* set next pointer NULL as sentinel */
        if (*n + 1 >= nptrs) {          /* check if realloc required */
            /* allocate using temporary pointer to prevent memory leak on failure */
            if (!(tmp = realloc (lines, 2 * nptrs * sizeof *lines))) {
                perror ("realloc-lines");
                return lines;           /* return original poiner on failure */
            }
            lines = tmp;                /* assign reallocated block to pointer */
            nptrs *= 2;                 /* update no. of pointers allocated */
        }
    }
    
    /* final realloc sizing exact no. of pointers required */
    if (!(tmp = realloc (lines, (*n + 1) * sizeof *lines)))
        return lines;   /* return original block on failure */
    
    return tmp;         /* return updated block of pointers on success */
}

请注意,该函数采用文件的 openFILE*参数,而不是采用文件名在函数内打开。您通常希望在调用函数中打开文件并验证它是否已打开以供读取,然后再调用函数以读取所有行。如果文件无法在调用者中打开,则没有理由让函数全部从文件中读取行开头。

通过一种从文件中读取存储所有行的方法,您接下来需要转向按长度对行进行排序,以便输出 5 条最短的行(引号)。由于您通常希望按顺序保留文件中的行,因此在保留原始顺序的同时按长度对行进行排序的最简单方法是制作指针的副本并按行长度对指针的副本进行排序。例如,您的lines指针可以继续按原始顺序包含指针,而指针集sortedlines可以按行长排序的顺序保存指针,例如

int main (int argc, char **argv) {
    
    char **lines = NULL,            /* pointer to allocated block of pointers */
         **sortedlines = NULL;      /* copy of lines pointers to sort by length */

读取文件并填充lines指针后,您可以将指针复制到sortedlines(包括哨兵NULL),例如

    /* alocate storage for copy of lines pointers (plus sentinel NULL) */
    if (!(sortedlines = malloc ((n + 1) * sizeof *sortedlines))) {
        perror ("malloc-sortedlines");
        return 1;
    }
    
    /* copy pointers from lines to sorted lines (plus sentinel NULL) */
    memcpy (sortedlines, lines, (n + 1) * sizeof *sortedlines);

然后你只需调用按长度qsort对指针进行排序。sortedlines你唯一的工作qsort就是编写 *compare` 函数。比较函数的原型是:

int compare (const void *a, const void *b);

两者ab都是指向被排序元素的指针。在您的情况下char **sortedlines;,元素将是指向字符的指针,因此a两者b都将具有指向指针的指针类型charless than zero您只需编写一个比较函数,如果指向的行的长度a小于(已经按正确的顺序),它将返回,如果b长度相同则返回零(无需操作),如果长度返回大于零ofa大于b(需要交换)。编写比较两个条件的差异而不是简单的a - b将防止所有潜在的溢出,例如

/** compare funciton for qsort, takes pointer-to-element in a & b */
int complength (const void *a, const void *b)
{
    /* a & b are pointer-to-pointer to char */
    char *pa = *(char * const *)a,      /* pa is pointer to string */
         *pb = *(char * const *)b;      /* pb is pointer to string */
    size_t  lena = strlen(pa),          /* length of pa */ 
            lenb = strlen(pb);          /* length of pb */
    
    /* for numeric types returing result of (a > b) - (a < b) instead
     * of result of a - b avoids potential overflow. returns -1, 0, 1.
     */
    return (lena > lenb) - (lena < lenb);
}

现在您可以简单地传递对象的集合、对象的数量、每个对象的大小以及用于对对象进行排序的函数qsort。你需要排序什么并不重要——它每次都以相同的方式工作。没有理由你应该“去写”一个排序(除了教育目的)——这qsort就是提供的。例如,在这里sortedlines,您只需要:

    qsort (sortedlines, n, sizeof *sortedlines, complength);    /* sort by length */

现在,您可以通过迭代lines显示所有行,并以升序的行长度显示所有行sortedlines。显然要显示前 5 行,只需遍历sortedlines. 这同样适用于打开另一个文件以将这 5 行写入新文件。(留给你)

而已。有没有困难——不。做起来很简单——不。这是C语言编程的一个基本部分,需要努力学习和理解,但这与任何值得学习的东西没有什么不同。将所有部分放在一个工作程序中以读取和显示文件中的所有行,然后排序并显示您可以执行的前 5 行最短的行:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

#define NSHORT 5        /* no. of shortest lines to display */
#define LINSZ 128       /* initial allocation size for each line */

/** compare funciton for qsort, takes pointer-to-element in a & b */
int complength (const void *a, const void *b)
{
    /* a & b are pointer-to-pointer to char */
    char *pa = *(char * const *)a,      /* pa is pointer to string */
         *pb = *(char * const *)b;      /* pb is pointer to string */
    size_t  lena = strlen(pa),          /* length of pa */ 
            lenb = strlen(pb);          /* length of pb */
    
    /* for numeric types returing result of (a > b) - (a < b) instead
     * of result of a - b avoids potential overflow. returns -1, 0, 1.
     */
    return (lena > lenb) - (lena < lenb);
}

/** read line from 'fp' stored in allocated block assinged to '*s' and
 *  return length of string stored on success, on EOF with no characters
 *  read, or on failure, return -1. Block of memory sized to accommodate
 *  exact length of string with nul-terminating char. unless -1 returned,
 *  *s guaranteed to contain nul-terminated string (empty-string allowed).
 *  caller responsible for freeing allocated memory.
 */
ssize_t fgetcline (char **s, FILE *fp)
{
    int c;                              /* char read from fp */
    size_t n = 0, size = LINSZ;         /* no. of chars and allocation size */
    void *tmp = realloc (NULL, size);   /* tmp pointer for realloc use */
    
    if (!tmp)       /* validate every allocation/reallocation */
        return -1;
    
    *s = tmp;       /* assign reallocated block to pointer */
    
    while ((c = fgetc(fp)) != '\n' && c != EOF) {   /* read chars until \n or EOF */
        if (n + 1 == size) {                        /* check if realloc required */
            /* realloc using temporary pointer */
            if (!(tmp = realloc (*s, size + LINSZ))) {
                free (*s);              /* on failure, free partial line */
                return -1;              /* return -1 */
            }
            *s = tmp;                   /* assign reallocated block to pointer */
            size += LINSZ;              /* update allocated size */
        }
        (*s)[n++] = c;                  /* assign char to index, increment */
    }
    (*s)[n] = 0;                        /* nul-terminate string */
    
    if (n == 0 && c == EOF) {   /* if nothing read and EOF, free mem return -1 */
        free (*s);
        return -1;
    }
    
    if ((tmp = realloc (*s, n + 1)))    /* final realloc to exact length */
        *s = tmp;                       /* assign reallocated block to pointer */
    
    return (ssize_t)n;      /* return length (excluding nul-terminating char) */
}

/** read each line from `fp` and store in allocated block returning pointer to
 *  allocateted block of pointers to each stored line with the final pointer
 *  after the last stored string set to NULL as a sentinel. 'n' is updated to
 *  the number of allocated and stored lines (excluding the sentinel NULL).
 *  returns valid pointer on success, NULL otherwise. caller is responsible for
 *  freeing both allocated lines and pointers.
 */
char **readfile (FILE *fp, size_t *n)
{
    size_t nptrs = LINSZ;               /* no. of allocated pointers */
    char **lines = malloc (nptrs * sizeof *lines);  /* allocated bock of pointers */
    void *tmp = NULL;                   /* temp pointer for realloc use */
    
    /* read each line from 'fp' into allocated block, assign to next pointer */
    while (fgetcline (&lines[*n], fp) != -1) {
        lines[++(*n)] = NULL;           /* set next pointer NULL as sentinel */
        if (*n + 1 >= nptrs) {          /* check if realloc required */
            /* allocate using temporary pointer to prevent memory leak on failure */
            if (!(tmp = realloc (lines, 2 * nptrs * sizeof *lines))) {
                perror ("realloc-lines");
                return lines;           /* return original poiner on failure */
            }
            lines = tmp;                /* assign reallocated block to pointer */
            nptrs *= 2;                 /* update no. of pointers allocated */
        }
    }
    
    /* final realloc sizing exact no. of pointers required */
    if (!(tmp = realloc (lines, (*n + 1) * sizeof *lines)))
        return lines;   /* return original block on failure */
    
    return tmp;         /* return updated block of pointers on success */
}

/** free all allocated memory (both lines and pointers) */
void freelines (char **lines, size_t nlines)
{
    for (size_t i = 0; i < nlines; i++) /* loop over each pointer */
        free (lines[i]);                /* free allocated line */
    
    free (lines);       /* free pointers */
}

int main (int argc, char **argv) {
    
    char **lines = NULL,            /* pointer to allocated block of pointers */
         **sortedlines = NULL;      /* copy of lines pointers to sort by length */
    size_t n = 0;                   /* no. of pointers with allocated lines */
    /* use filename provided as 1st argument (stdin by default) */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;

    if (!fp) {  /* validate file open for reading */
        perror ("file open failed");
        return 1;
    }
    
    if (!(lines = readfile (fp, &n)))   /* read all lines in file, fill lines */
        return 1;
    
    if (fp != stdin)                /* close file if not stdin */
        fclose (fp);
    
    /* alocate storage for copy of lines pointers (plus sentinel NULL) */
    if (!(sortedlines = malloc ((n + 1) * sizeof *sortedlines))) {
        perror ("malloc-sortedlines");
        return 1;
    }
    
    /* copy pointers from lines to sorted lines (plus sentinel NULL) */
    memcpy (sortedlines, lines, (n + 1) * sizeof *sortedlines);
    
    qsort (sortedlines, n, sizeof *sortedlines, complength);    /* sort by length */
    
    /* output all lines from file (first screen) */
    puts ("All lines:\n\nline : text");
    for (size_t i = 0; i < n; i++)
        printf ("%4zu : %s\n", i + 1, lines[i]);
    
    /* output first five shortest lines (second screen) */
    puts ("\n5 shortest lines:\n\nline : text");
    for (size_t i = 0; i < (n >= NSHORT ? NSHORT : n); i++)
        printf ("%4zu : %s\n", i + 1, sortedlines[i]);
    
    freelines (lines, n);       /* free all allocated memory for lines */
    free (sortedlines);         /* free block of pointers */
}

注意:文件从作为第一个参数传递给程序的文件名中读取,stdin如果没有给出参数,则从文件名中读取)

示例输入文件

$ cat dat/fleascatsdogs.txt
My dog
My fat cat
My snake
My dog has fleas
My cat has none
Lucky cat
My snake has scales

示例使用/输出

$ ./bin/fgetclinesimple dat/fleascatsdogs.txt
All lines:

line : text
   1 : My dog
   2 : My fat cat
   3 : My snake
   4 : My dog has fleas
   5 : My cat has none
   6 : Lucky cat
   7 : My snake has scales

5 shortest lines:

line : text
   1 : My dog
   2 : My snake
   3 : Lucky cat
   4 : My fat cat
   5 : My cat has none

内存使用/错误检查

在您编写的任何动态分配内存的代码中,对于分配的任何内存块,您有 2 个责任:(1)始终保留指向内存块起始地址的指针,(2)它可以在它不存在时被释放更需要。

您必须使用内存错误检查程序,以确保您不会尝试访问内存或写入超出/超出分配块的范围,尝试读取或基于未初始化值的条件跳转,最后确认释放所有分配的内存。

对于 Linuxvalgrind是正常的选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。

$ valgrind ./bin/fgetclinesimple dat/fleascatsdogs.txt
==5900== Memcheck, a memory error detector
==5900== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5900== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5900== Command: ./bin/fgetclinesimple dat/fleascatsdogs.txt
==5900==
All lines:

line : text
   1 : My dog
   2 : My fat cat
   3 : My snake
   4 : My dog has fleas
   5 : My cat has none
   6 : Lucky cat
   7 : My snake has scales

5 shortest lines:

line : text
   1 : My dog
   2 : My snake
   3 : Lucky cat
   4 : My fat cat
   5 : My cat has none
==5900==
==5900== HEAP SUMMARY:
==5900==     in use at exit: 0 bytes in 0 blocks
==5900==   total heap usage: 21 allocs, 21 frees, 7,938 bytes allocated
==5900==
==5900== All heap blocks were freed -- no leaks are possible
==5900==
==5900== For counts of detected and suppressed errors, rerun with: -v
==5900== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

始终确认您已释放所有已分配的内存并且没有内存错误。

这里有很多,就像任何“它如何做X?”一样。问题,魔鬼总是在细节,每个功能的正确使用,每个输入或分配/重新分配的正确验证。每个部分都与其他部分一样重要,以确保您的代码以定义的方式完成您需要它做的事情。仔细检查一下,花点时间消化这些部分,如果您还有其他问题,请告诉我。


推荐阅读