从您的问题和通过 cmets 可以看出,您希望将文件中的所有引号(行)读入动态分配的存储(屏幕 1),然后按长度对行进行排序并输出前 5 个最短的行(屏幕 2) 将 5 条最短的行保存到第二个输出文件中(这部分留给您)。从文件中读取和存储所有行并不困难——但也不是微不足道的。这听起来很基础,但确实如此,但它要求您正确使用与持久存储(从磁盘/存储介质读取文件)和计算机内存子系统 (RAM) 交互所需的所有基本工具。
从文件中读取每一行并不难,但就像 C 语言中的任何内容一样,它需要您注意细节。您可以使用面向字符的输入函数(fgetc()、getc()等)从文件中读取,也可以使用格式化输入函数(@987654323 @) 并且您可以使用 面向行的 输入函数,例如 (fgets() 或 POSIX getline())。从文件中读取行通常是使用面向行的 函数完成的,但使用面向字符的 方法也没有错。事实上,您可以相对轻松地编写一个基于 fgetc() 的函数,该函数将为您读取文件中的每一行。
在您知道文件中最长行的最大字符数的普通情况下,您可以使用 2D 字符数组来存储整个文件。这通过消除动态分配存储空间的需要简化了该过程,但有许多缺点,例如文件中的每一行需要与文件中最长的行相同的存储空间,并且通过限制可以存储的文件大小到程序堆栈的大小。使用(malloc、calloc 或 realloc)动态分配存储可以消除这些缺点和低效率,从而使您可以将文件存储到计算机上可用内存的限制。 (这里有一些方法可以通过使用滑动窗口技术来处理任何大小的文件,这远远超出了您的需要)
处理动态分配的内存,或者逐个字符地在其中复制或存储数据并不难。也就是说,每个分配的责任,跟踪写入每个分配块的数据量,重新分配以调整块大小以确保没有数据写入每个块的边界之外,然后在不再需要时释放每个分配的块 - - 是你的,程序员。 C 赋予程序员使用每个可用内存字节的权力,同时也赋予程序员正确使用内存的责任。
存储文件的基本方法很简单。您从文件中读取每一行,为每个字符分配/重新分配存储空间,直到遇到 '\n' 或 EOF。为了协调所有行,您分配一个指针块,然后将每个包含行的内存块的地址分配给一个指针,依次重新分配保存所有行所需的指针数量。
有时一张图片真的值 1000 字。使用基本方法,您声明一个指针(指向什么?)一个指针,以便您可以分配一个包含指针的内存块,您将为每个分配的行分配指针。例如,您可以声明,char **lines; A pointer-to-pointer 是指向包含指针的内存块的单个指针。然后lines 的每个指针的类型将是char *,它将指向每个包含文件中一行的块,例如
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 |
+----+ +----------------+
您可以通过分配 1 个额外的指针并将该指针初始化为 NULL 来使 lines 的使用更加灵活,这允许您在不知道有多少行的情况下迭代 lines -- 直到 NULL遇到,例如
| .. | | ... |
+----+ +-------------------+
| pn | --> | last line read |
+----+ +----------------+
|pn+1| | NULL |
+----+ +------+
虽然您可以将所有这些放在一个函数中,以帮助学习过程(并且只是为了实际可重用性),但将其分解为两个函数通常更容易。一个为每一行读取和分配存储空间,第二个函数基本上调用第一个函数,分配指针并将每个分配的内存块的地址分配给从文件中读取的行依次分配给下一个指针。完成后,您将获得一个已分配的指针块,其中每个指针都保存(指向)已分配块的地址,该块包含文件中的一行。
您已表示要使用fgetc() 从文件中读取并一次读取一个字符。这并没有什么问题,而且这种方法几乎没有任何损失,因为底层 I/O 子系统提供了一个读取缓冲区,您实际上是从中读取而不是一次从磁盘读取一个字符。 (大小因编译器而异,但一般通过BUFSIZ宏提供,Linux和Windows编译器都提供这个)
实际上有无数种方法可以编写一个函数,该函数分配存储空间来保存一行,然后从文件中读取一行,一次一个字符,直到遇到'\n' 或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 终止 字符所需的确切字符数。 p>
调用该函数以读取文件中的所有行,同时根据需要分配和重新分配指针,其作用与上述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 */
}
请注意,该函数采用打开文件的FILE* 参数,而不是采用在函数内打开的文件名。您通常希望在调用函数中打开文件并验证它是否已打开以供读取,然后再调用函数以读取所有行。如果文件无法在调用者中打开,则没有理由让函数全部从文件中读取行开头。
通过一种从文件中读取存储所有行的方法,您接下来需要转向按长度对行进行排序,以便输出 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);
a 和b 都将是pointers-to 元素被排序。在您使用 char **sortedlines; 的情况下,元素将是 pointer-to-char,因此 a 和 b 都将具有类型 pointer-to-pointer char。您只需编写一个比较函数,如果a 指向的行的长度小于b(已经按照正确的顺序),它将返回less than zero,如果长度相同则返回零(无需操作) 如果a 的长度大于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 个有效指针。这同样适用于打开另一个文件以将这 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) 当不再需要它时可以释放。
您必须使用内存错误检查程序来确保您不会尝试访问内存或写入超出/超出分配块的边界,尝试读取或基于未初始化的值进行条件跳转,最后, 以确认您已释放所有已分配的内存。
对于 Linux,valgrind 是正常的选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。
$ 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?”一样。问题,魔鬼总是在细节,每个功能的正确使用,每个输入或分配/重新分配的正确验证。每个部分都与其他部分一样重要,以确保您的代码以定义的方式完成您需要它做的事情。仔细查看,花点时间消化这些部分,如果您还有其他问题,请告诉我。