您提出的问题是“我如何从文件中读取未知数量的未知长度的行?”的经典问题您以内存有效的方式解决问题的方法是声明一个指向char 的指针并分配合理数量的指针以开始,然后为每一行分配起始地址,当您读取每一行并为其分配时,将保存每一行的块的起始地址分配给您的指针。
一种有效的方法是使用 fgets 或使用 POSIX getline 将文件中的每一行读入一个固定缓冲区(大小足以容纳最长的行而不会跳过),这将根据需要分配给坚持到底。然后,您从临时缓冲区中删除尾随 '\n' 并获取该行的长度。
然后分配length + 1 字符的内存块(+1 用于 nul-terminating 字符)并将新内存块的地址分配给下一个可用指针(保持跟踪分配的指针数量和使用的指针数量)
当使用的指针数量等于分配的数量时,您只需 realloc 附加指针(通常通过将当前可用数量加倍,或分配一些固定数量的附加指针)然后继续。根据需要重复该过程多次,直到您读取了输入文件中的所有行。
有很多方法可以实现它并安排不同的任务,但基本上都归结为同一件事。从一个合理大小的临时缓冲区开始处理最长的行(不要吝啬,以防输入数据有一些变化——1K 的缓冲区是便宜的保险,根据需要进行调整)。添加您的计数器以跟踪分配的指针数量以及使用的数量(您的索引)。打开并验证命令行上给出的文件是否打开以供读取(如果命令行上没有给出参数,则默认从stdin读取)例如,您可以这样做:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXC 1024 /* if you need a constant, #define one (or more) */
int main (int argc, char **argv) {
char buf[MAXC] = "", /* temp buffer to hold line read from file */
**lines = NULL; /* pointer-to-pointer to each line */
size_t ndx = 0, alloced = 0; /* current index, no. of ptrs allocated */
/* 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;
}
...
打开并验证文件后,您已准备好读取每一行,使用 read 函数本身控制读取循环,并按照上面的大纲处理每一行的存储,例如
while (fgets (buf, MAXC, fp)) { /* read each line */
size_t len; /* for line length */
if (ndx == alloced) { /* check if realloc needed */
void *tmp = realloc (lines, /* alloc 2X current, or 2 1st time */
(alloced ? 2 * alloced : 2) * sizeof *lines);
if (!tmp) { /* validate EVERY allocation */
perror ("realloc-lines");
break; /* if allocation failed, data in lines still good */
}
lines = tmp; /* assign new block of mem to lines */
alloced = alloced ? 2 * alloced : 2; /* update ptrs alloced */
}
注意: 上面,在你的读取循环中发生的第一件事是检查你是否有可用的指针,例如if (ndx == alloced),如果您的索引(使用的编号)等于分配的编号,则您重新分配更多。 alloced ? 2 * alloced : 2 上面的 三元 只是询问您是否有一些先前分配的 alloced ? 然后将 2 * alloced 的数字加倍,否则(:)只需从 2 指针开始并从那里开始。在该加倍方案中,您在每次连续重新分配时分配 2, 4, 8, 16, ... 指针。
另请注意:当您调用 realloc 时,您始终使用临时指针,例如tmp = realloc (lines, ...) 而你从不 realloc 使用指针本身,例如lines = realloc (lines, ...)。当(不是如果)realloc 失败时,它返回NULL,如果你将它分配给你的原始指针——你刚刚创建了一个内存泄漏,因为lines 的地址已经丢失,这意味着你无法到达或@ 987654345@你之前分配的内存。
现在您已经确认您有一个可用的指针来分配内存块的地址来保存该行,从buf 中删除'\n' 并获取该行的长度。您可以在一次调用 strcspn 时方便地做到这一点,它返回字符串中不包含分隔符 "\n" 的初始字符数,例如
buf[(len = strcspn(buf, "\n"))] = 0; /* trim \n, get length */
(注意:上面你只是用空终止字符0覆盖'\n',相当于'\0')
现在您有了行的长度,您只需分配 length + 1 字符并从临时缓冲区 buf 复制到您的新内存块,例如
if (!(lines[ndx] = malloc (len + 1))) { /* allocate for lines[ndx] */
perror ("malloc-lines[ndx]"); /* validate combined above */
break;
}
memcpy (lines[ndx++], buf, len + 1); /* copy buf to lines[ndx] */
} /* increment ndx */
此时您已完成所有行的读取和存储,如果未从 stdin 读取,则可以简单地关闭文件。在这里,例如,我们只是输出每一行,然后free 为每一行存储,最后也为分配的指针释放内存,例如
if (fp != stdin) fclose (fp); /* close file if not stdin */
for (size_t i = 0; i < ndx; i++) { /* loop over each storage line */
printf ("lines[%2zu] : %s\n", i, lines[i]); /* output line */
free (lines[i]); /* free storage for strings */
}
free (lines); /* free pointers */
}
就是这样。总而言之,您可以这样做:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXC 1024 /* if you need a constant, #define one (or more) */
int main (int argc, char **argv) {
char buf[MAXC] = "", /* temp buffer to hold line read from file */
**lines = NULL; /* pointer-to-pointer to each line */
size_t ndx = 0, alloced = 0; /* current index, no. of ptrs allocated */
/* 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;
}
while (fgets (buf, MAXC, fp)) { /* read each line */
size_t len; /* for line length */
if (ndx == alloced) { /* check if realloc needed */
void *tmp = realloc (lines, /* alloc 2X current, or 2 1st time */
(alloced ? 2 * alloced : 2) * sizeof *lines);
if (!tmp) { /* validate EVERY allocation */
perror ("realloc-lines");
break; /* if allocation failed, data in lines still good */
}
lines = tmp; /* assign new block of mem to lines */
alloced = alloced ? 2 * alloced : 2; /* update ptrs alloced */
}
buf[(len = strcspn(buf, "\n"))] = 0; /* trim \n, get length */
if (!(lines[ndx] = malloc (len + 1))) { /* allocate for lines[ndx] */
perror ("malloc-lines[ndx]"); /* validate combined above */
break;
}
memcpy (lines[ndx++], buf, len + 1); /* copy buf to lines[ndx] */
} /* increment ndx */
if (fp != stdin) fclose (fp); /* close file if not stdin */
for (size_t i = 0; i < ndx; i++) { /* loop over each storage line */
printf ("lines[%2zu] : %s\n", i, lines[i]); /* output line */
free (lines[i]); /* free storage for strings */
}
free (lines); /* free pointers */
}
使用/输出示例
$ ./bin/fgets_lines_dyn dat/cmdlinefile.txt
lines[ 0] : A 1 2 3 4 5
lines[ 1] : B 0 0
lines[ 2] : C 1 1
从stdin 重定向而不是打开文件:
$ ./bin/fgets_lines_dyn < dat/cmdlinefile.txt
lines[ 0] : A 1 2 3 4 5
lines[ 1] : B 0 0
lines[ 2] : C 1 1
内存使用/错误检查
在您编写的任何动态分配内存的代码中,对于分配的任何内存块,您都有 2 个职责:(1)始终保留指向起始地址的指针内存块,因此,(2) 当不再需要它时可以释放。
您必须使用内存错误检查程序来确保您不会尝试访问内存或写入超出/超出分配块的边界,尝试读取或基于未初始化的值进行条件跳转,最后,以确认您释放了已分配的所有内存。
对于 Linux,valgrind 是正常的选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。
$ valgrind ./bin/fgets_lines_dyn dat/cmdlinefile.txt
==6852== Memcheck, a memory error detector
==6852== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==6852== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==6852== Command: ./bin/fgets_lines_dyn dat/cmdlinefile.txt
==6852==
lines[ 0] : A 1 2 3 4 5
lines[ 1] : B 0 0
lines[ 2] : C 1 1
==6852==
==6852== HEAP SUMMARY:
==6852== in use at exit: 0 bytes in 0 blocks
==6852== total heap usage: 6 allocs, 6 frees, 624 bytes allocated
==6852==
==6852== All heap blocks were freed -- no leaks are possible
==6852==
==6852== For counts of detected and suppressed errors, rerun with: -v
==6852== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
始终确认您已释放已分配的所有内存并且没有内存错误。
虽然为每个指针和每一行分配存储空间一开始可能看起来令人生畏,但无论是从文件中读取和存储行、整数还是浮点值,您都会一遍又一遍地面临这个问题。数据等……值得花时间学习。您的替代方法是声明一个固定大小的二维数组,并希望您的行长永远不会超过声明的宽度,并且行数永远不会超过您声明的行数。 (您也应该了解这一点,但限制很快就会显现出来)
检查一下,如果您还有其他问题,请告诉我。