将字符串数组传递给 fscanf 的函数

Passing an array of strings into a function for fscanf

当我 运行 此代码出现分段错误时,我不知道为什么会这样,我试图从文本文件的每一行中读取姓名和现金金额,然后将其放入成一个数组。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void ReadFile(int *cash, char **name)
{
    int r, line = 0;

    FILE *fp;
    fp = fopen("coins.txt", "r");

    if (fp == NULL)
    {
        printf ("Error opening the file\n\n'");
        exit(EXIT_FAILURE);
    } else {
        while (line <= 14)
        {
            
            r = fscanf(fp, "%s %d\n", name[line], &cash[line]);
            if (r != 2)
            {
                printf ("Error, line %d in wrong format!\n\n", line);
            }
            //printf("Name: %s, Cash: $%d\n",name, *cash);
            line++;
        }
    fclose(fp);
    }
    
}

int main()
{
    int cash[14];
    char *name[14] = { "" };
    ReadFile(cash,name);
    //printf("%s\n", name[0]);
}

gdb 非常适合查找此类问题。

segfault$ gcc -ggdb segfault.c -o segfault
segfault$ gdb segfault 
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
[...]
Reading symbols from segfault...done.
(gdb) break segfault.c:20
Breakpoint 1 at 0x896: file segfault.c, line 20.
(gdb) run
Starting program: /tmp/segfault/segfault 

Breakpoint 1, ReadFile (cash=0x7fffffffdd80, name=0x7fffffffddc0) at segfault.c:20
20              r = fscanf(fp, "%s %d\n", name[line], &cash[line]);
(gdb) print line
 = 0
(gdb) print &cash[line]
 = (int *) 0x7fffffffdd80
(gdb) print name[line]
 = 0x0

main() 中使用 char *name[14],您正在创建一个 char 指针数组,但您没有设置指针。因此,它们可能指向任何地方,最有可能指向无效位置(当您尝试 read/write 该位置时会导致段错误)。

在我的系统上,我们可以看到我们很幸运,未初始化的 name[0] 指向 0x0 (NULL)。这并不能保证,我们可能会遇到比这里的段错误更有趣的错误。想象一下,例如,如果 name[0] 改为指向内存中存储 line 的位置。你会覆盖那个内存,并从你的循环中得到非常奇怪的行为。

您必须初始化 name 数组中的每个指针(在 main() 中),以便它们都指向足够大小的有效区域。

继续评论,直接错误是由于 char *name[14] 声明了一个包含 14 个指向 char 的指针的数组,这些指针 未初始化 .这意味着每个指针都指向任何地方(例如,指针所持有的地址,因为它的值指向某个不确定的内存地址)。在你可以存储任何东西之前,你必须确保你有一个有效的内存位置来保存你正在处理的任何值。对于字符串,例如 name[x],这意味着您需要 length + 1 个字符可用(+1nul-terminating 字符提供存储, '[=21=]', 相当于普通的 0)

您的 ReadFile() 功能严重不足,您的阅读能力很脆弱。任何时候读取数据行时,都应该使用 面向行的 输入函数,例如 fgets()(或 POSIX getline())。这样你每次都会读取(消耗)一行输入,任何导致 匹配失败 的输入文件格式差异只会影响那一行,而不会破坏文件的读取从那时起。

永远不要硬编码文件名 或在代码中使用MagicNumbers。你不应该仅仅为了从不同的输入文件中读取而重新编译你的程序。您可以提供默认使用的文件名,否则将文件名作为程序的参数读取(这就是 int argc, char **argv 的用途),或者提示用户并将文件名作为输入。为了避免代码中的硬编码文件名和幻数:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define COINS  14       /* if you need a constant, #define one (or more) */
#define MAXC 1024

int main (int argc, char **argv)
{
    ...
    FILE *fp = fopen (argc > 1 ? argv[1] : "coins.txt", "r");
    ...

此外,您通常希望在调用函数中打开并验证文件是否已打开以供读取,并将 FILE* 指针作为函数的参数传递给打开的流。如果文件打开失败,则无需调用函数开头。

将所有部分放在一起并使用 MAXC 个字符的缓冲区来保存在转换(并验证)转换之前从文件中读取的每一行。 (很好地验证了 fscanf() 中的 return)。

要解决您的问题,您应该将 name 值读入临时数组,然后分配存储空间,只需将临时数组中字符串的 strlen()malloc (len + 1) 个字符用于 name[i](验证每个分配),然后从临时数组复制到 name[i]。 (当你完成调用者的值后,你将需要释放内存(main() 这里)

您还需要 lineindex 的单独计数器。由于您报告了发生任何故障的行(再次干得好!),因此您需要在每次迭代时增加 line 计数器,但您只想增加 name 和 [= 的索引40=] 表示成功转换,例如

size_t ReadFile (FILE *fp, int *cash, char **name)
{
    char buf[MAXC];                     /* temporary array to hold line */
    size_t index = 0, line = 0;         /* separate counters for index and line */
    
    while (line < COINS && fgets (buf, MAXC, fp))   /* protect array, read line */
    {
        char coinname[MAXC];            /* temporary array for name */
        int r, value;                   /* declare variables in scope required */
        size_t len;                     /* length of name */
        
        r = sscanf (buf, "%1023s %d", coinname, &value);    /* convert values */
        if (r != 2) {   /* validate conversion */
            fprintf (stderr, "Error, line %zu in wrong format!\n\n", line);
            line++;     /* don't forget to increment line */
            continue;   /* before continuing to read next line */
        }
        
        len = strlen (coinname);                    /* get length of name */
        name[index] = malloc (len + 1);             /* allocate len + 1 chars */
        if (name[index] == NULL) {                  /* validate allocation */
            perror ("malloc-name[index]");
            return index;
        }
        memcpy (name[index], coinname, len + 1);    /* copy to name[index] */
        cash[index] = value;                        /* assign to cash[index] */
        
        index++;        /* increment index */
        line++;         /* increment line */
    }
    
    return index;       /* return index */
}

(注意: 将 return 类型从 void 更改为 size_t 以启用 return从文件中读取的名称和值的数量。始终为任何可以成功或失败的函数提供有意义的 return 类型,例如,除了简单的打印或自由函数之外的任何东西)

您还有另一个存储 name 的选项。您可以将 name 声明为二维字符数组,而不是使用 malloc()(或 calloc()realloc())分配存储空间,每行的存储空间足以容纳最长的名称(+1) 个字符。这取决于名称之间的长度变化,可能需要比完全分配以容纳每个 name 多得多的存储空间。它通过消除分配的需要确实简化了一些事情。由你决定。

现在您可以将函数中的 return 分配给 main() 中的变量,这样您就可以知道有多少对 namecash 被成功读取,例如

int main (int argc, char **argv)
{
    int cash[COINS];
    char *name[COINS] = {NULL};
    size_t ncoins = 0;
    /* use filename provided as 1st argument (coins.txt by default) */
    FILE *fp = fopen (argc > 1 ? argv[1] : "coins.txt", "r");
    
    if (!fp) {  /* validate file open for reading */
        perror ("file open failed");
        return 1;
    }
    
    ncoins = ReadFile (fp, cash, name);
    fclose (fp);
    
    for (size_t i = 0; i < ncoins; i++) {
        printf ("%-12s %3d\n", name[i], cash[i]);
        free (name[i]);                             /* free mem when done */
    }
}

示例输入文件

$ cat dat/name_coins.txt
penny 20
nickle 3
dime 8
quarter 15
half-dollar 5
dollar 4

示例Use/Output

$ ./bin/name_coins dat/name_coins.txt
penny         20
nickle         3
dime           8
quarter       15
half-dollar    5
dollar         4

内存Use/Error检查

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

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

对于Linux valgrind是正常的选择。每个平台都有类似的内存检查器。它们都很简单易用,只需运行你的程序就可以了。

$ valgrind ./bin/name_coins dat/name_coins.txt
==5870== Memcheck, a memory error detector
==5870== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5870== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5870== Command: ./bin/name_coins dat/name_coins.txt
==5870==
penny         20
nickle         3
dime           8
quarter       15
half-dollar    5
dollar         4
==5870==
==5870== HEAP SUMMARY:
==5870==     in use at exit: 0 bytes in 0 blocks
==5870==   total heap usage: 9 allocs, 9 frees, 5,717 bytes allocated
==5870==
==5870== All heap blocks were freed -- no leaks are possible
==5870==
==5870== For counts of detected and suppressed errors, rerun with: -v
==5870== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

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

检查一下,如果您还有其他问题,请告诉我。

此外,您通常需要打开并验证报告是否在调用函数内部打开以供研究,并绕过 FILE* 指针指向打开循环作为函数的争议 page。如果文档打开失败,就不用先做特征名了。