第一个问题是您只为单词列表(即字符指针)分配空间,但您没有为单词本身分配空间。
char** words = (char**)malloc(20 * sizeof(char*));
这会为 20 个字符指针分配空间并将其分配给 words。现在words[i] 有空间存放字符指针但没有存放字符。
words[i] 包含垃圾,因为malloc 不初始化内存。当您将其传递给fscanf 时,fscanf 会尝试使用words[i] 中的垃圾作为写入字符的内存位置。这要么会破坏程序中的一些内存,要么更可能是it tries to read a memory location is isn't allowed to and crashes。不管怎样,都不好。
您必须为字符串分配内存,将其传递给fscanf,最后将该字符串放入words[i]。
char** words = malloc(numWords * sizeof(char*));
for(i = 0; i < numWords; i++) {
char *word = malloc(40 * sizeof(char));
fscanf(ifp, "%39s", word);
words[i] = word;
printf("%s\n", words[i]);
}
请注意,我没有转换 malloc、that's generally considered unnecessary 的结果。
还要注意我在列表中为numWords 分配了空间。您的原始文件仅分配 20 个字的空间,一旦超出,它将开始覆盖分配的内存并可能崩溃。根据经验,避免持续分配内存。尽快适应动态内存分配。
还请注意,我将允许读取的 fscanf 的字符数限制为缓冲区的大小(由于字符串末尾的空字节而减一)。否则,如果您的单词列表包含 45 个字符的“Pneumonoultramicroscopicsilicovolcanoconiosis”,它会超出 word 缓冲区并开始在相邻元素上乱涂乱画,这会很糟糕。
这导致fscanf 和scanf 常见的一个新问题:部分读取。当上面的代码遇到“Pneumonoultramicroscopicsilicovolcanoco”时,fscanf(ifp, "%39s", word); 会在前 39 个字符中读取“Pneumonoultramicroscopicsilicovolcanoco”并停止。对fscanf 的下一次调用将显示“niosis”。您将存储和打印它们,就好像它们是两个单词一样。这不好。
你可以通过增大单词缓冲区来解决这个问题,但是现在大多数单词会浪费很多内存。
scanf and fscanf have a whole lot of problems and are best avoided。相反,最好阅读整行并使用sscanf 解析它们。在这种情况下,您不需要进行任何解析,它们只是字符串,因此获取该行就足够了。
fgets 是读取一行的常用方法,但这也需要您尝试猜测您需要在该行中读取多少内存。为了缓解这种情况,请使用大的行缓冲区,并从中复制单词。
void strip_newline( char* string ) {
size_t len = strlen(string);
if( string[len-1] == '\n' ) {
string[len-1] = '\0';
}
}
...
int i;
/* The word list */
char** words = malloc(numWords * sizeof(char*));
/* The line buffer */
char *line = malloc(1024 * sizeof(char*));
for(i = 0; i < numWords; i++) {
/* Read into the line buffer */
fgets(line, 1024, ifp);
/* Strip the newline off, fgets() doesn't do that */
strip_newline(line);
/* Copy the line into words */
words[i] = strdup(line);
printf("%s\n", words[i]);
}
strdup 不会复制所有 1024 个字节,对于单词来说已经足够了。这将导致只使用您需要的内存。
假设文件有一定数量的行,这会导致问题。 即使文件说它包含一定数量的行,您仍然应该验证这一点。否则,当您尝试读取文件末尾时会出现奇怪的错误。在这种情况下,如果文件少于numWords,它将尝试读取垃圾并可能崩溃。相反,您应该阅读该文件,直到没有更多行为止。
通常这是通过在 while 循环中检查 fgets 的返回值来完成的。
int i;
for( i = 0; fgets(line, 1024, ifp) != NULL; i++ ) {
strip_newline(line);
words[i] = strdup(line);
printf("%s\n", words[i]);
}
这就带来了一个新问题,我们怎么知道words有多大?你没有。这给我们带来了增长和重新分配内存。这个答案太长了,我就画个草图吧。
char **readDictionary(FILE *ifp) {
/* Allocate a decent initial size for the list */
size_t list_size = 256;
char** words = malloc(list_size * sizeof(char*));
char *line = malloc(1024 * sizeof(char*));
size_t i;
for( i = 0; fgets(line, 1024, ifp) != NULL; i++ ) {
strip_newline(line);
/* If we're about to overflow the list, double its size */
if( i > list_size - 1 ) {
list_size *= 2;
words = realloc( words, list_size * sizeof(char*));
}
words[i] = strdup(line);
}
/* Null terminate the list so readers know when to stop */
words[i] = NULL;
return words;
}
int main() {
FILE *fp = fopen("/usr/share/dict/words", "r");
char **words = readDictionary(fp);
for( int i = 0; words[i] != NULL; i++ ) {
printf("%s\n", words[i]);
}
}
现在列表将从 256 开始并根据需要增长。加倍增长非常快,不会浪费太多内存。我的 /usr/share/dict/words 里面有 235886 行。这可以存储在 218 或 262144 中。256 是 28,因此它只需要对 realloc 进行 10 次昂贵的调用即可增长到必要的大小。
我已将其更改为返回列表,因为如果您只是要立即使用它,那么构建列表并没有什么好处。这使我能够演示另一种处理动态大小列表的技术,即空终止。列表中的最后一个元素设置为NULL,因此任何阅读列表的人都知道何时停止。这比尝试通过列表传递长度更安全、更简单。
这很多,但这就是在 C 语言中处理文件时需要做的所有基本工作。手动完成是件好事,但幸运的是,有一些库可以让这种事情变得更容易。例如,Gnome Lib provides a lot of basic functionality 包括 arrays of pointers that automatically grow as needed。