【问题标题】:C - Saving strings into array elementsC - 将字符串保存到数组元素中
【发布时间】:2017-01-25 19:51:15
【问题描述】:

我有一个大约 150,000 个单词的记事本文件(代表字典)。我正在尝试扫描每个单词并将其打印到控制台。此设置工作正常:

void readDictionary(FILE *ifp, int numWords) {
    fscanf(ifp, "%d", &numWords);
    printf("%d\n", numWords);

    int i;
    char* words = (char*)malloc(20 * sizeof(char));
    for(i = 0; i < numWords; i++) {
        fscanf(ifp, "%s", words);
        printf("%s\n", words);
    }
}

然而,这段代码显然在每次循环时都会覆盖“单词”。我试图让每个单词保存到某个数组元素。我做了以下操作,但它立即崩溃了(我将内存分配更改为 2D,因为我在这里阅读,这似乎是我应该做的):

void readDictionary(FILE *ifp, int numWords) {
    fscanf(ifp, "%d", &numWords);
    printf("%d\n", numWords);

    int i;
    char** words = (char**)malloc(20 * sizeof(char*));
    for(i = 0; i < numWords; i++) {
        fscanf(ifp, "%s", words[i]);
        printf("%s\n", words[i]);
    }
}

感谢任何帮助。看了很多帖子还是没搞明白。

【问题讨论】:

  • 在更高的概念级别上,您可能需要查看字典或哈希表数据结构来解决此问题。
  • 第二个代码 sn-p 为 20 个指针分配空间,而不是 150000。它不为每个单词的字符分配空间。
  • “我将内存分配更改为 2D” - 你没有。那不是二维数组,而是锯齿状数组。指针不是数组!

标签: c arrays string file memory


【解决方案1】:

在您的第二个版本中,您为 20 个指针分配了空间,但这些指针未初始化且没有任何可指向的内容。我敢肯定,当您尝试从字典中读取这些指针之一指定的内存时,您可以想象这会带来什么问题。

您似乎想为numwords 指针分配空间

char** words = malloc(numwords * sizeof(*words));

,并为每个单词分配空间。

for(i = 0; i < numWords; i++) {
    words[i] = malloc(20);  //  by definition, sizeof(char) == 1
    // ...

另外,检查malloc()的返回值,如果分配失败,返回值为NULL

【讨论】:

  • 谢谢!我对内存分配部分感到迷茫,但按照您的解释方式,这是有道理的。
【解决方案2】:

第一个问题是您只为单词列表(即字符指针)分配空间,但您没有为单词本身分配空间。

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]);
}

请注意,我没有转换 mallocthat's generally considered unnecessary 的结果。

还要注意我在列表中为numWords 分配了空间。您的原始文件仅分配 20 个字的空间,一旦超出,它将开始覆盖分配的内存并可能崩溃。根据经验,避免持续分配内存。尽快适应动态内存分配。


还请注意,我将允许读取的 fscanf 的字符数限制为缓冲区的大小(由于字符串末尾的空字节而减一)。否则,如果您的单词列表包含 45 个字符的“Pneumonoultramicroscopicsilicovolcanoconiosis”,它会超出 word 缓冲区并开始在相邻元素上乱涂乱画,这会很糟糕。

这导致fscanfscanf 常见的一个新问题:部分读取。当上面的代码遇到“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

【讨论】:

  • 感谢您提供大量有用的信息!至于最后一部分,我确实需要按照教授的要求手动完成这些事情,但我认为这是为了让我们更熟悉语言本身。
  • @Josh 是的,自己做几次很好。然后,当涉及到真正的代码时,请使用库。
猜你喜欢
  • 2012-09-26
  • 1970-01-01
  • 2021-02-26
  • 1970-01-01
  • 1970-01-01
  • 2011-06-22
  • 1970-01-01
  • 1970-01-01
  • 2015-06-24
相关资源
最近更新 更多