从文件中读取数据时出现分段错误

Segmentation fault while reading data from file

我在用 C 语言解析 CSV 文件中的数据时遇到分段错误。 我相信在阅读最后一行 时给出了错误,就好像我评论代码运行完美的同一行一样。

CSV 文件的内容:

1;A;John Mott;D;30;Z
2;B;Judy Moor;S;60;X
3;A;Kae Blanchett;S;42;y
4;B;Jair Tade;S;21;W
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct Person
{
    int id;
    char key;
    char name[16];
    char rel;
    int age;
    char status;
} Person;

int main()
{
    Person person[12];

    FILE *f = fopen("data.csv", "r");
    char buffer[256];

    if (f != NULL)
    {
        int i = 0;
        printf("\nFile OK!\n");
        printf("Printing persons:\n");
        while (fgets(buffer, 256, f))
        {
            person[i].id = atoi(strtok(buffer, ";"));
            person[i].key = strtok(NULL, ";")[0];
            strcpy(person[i].name, strtok(NULL, ";"));
            person[i].rel = strtok(NULL, ";")[0];
            person[i].age = atoi(strtok(buffer, ";"));
            person[i].status = strtok(NULL, ";")[0]; // error: segmentation fault

            printf("id: %d\n", person[i].id);
            printf("key: %c\n", person[i].key);
            printf("name: %s\n", person[i].name);
            printf("rel: %c\n", person[i].rel);
            printf("age: %d\n", person[i].age);
            printf("status: %c\n", person[i].status);
            i++;
        }
    }
    else
    {
        printf("\nFile BAD!\n");
    }

    return 0;
}

感谢您的帮助!

虽然您对 strtok() 的问题有很好的解决方案,但您一开始就使用 strtok() 可能会使您的代码过于复杂。当读取带有固定分隔符的分隔文件时,一次读取一行到足够大小的缓冲区,然后使用 sscanf() 将缓冲区分隔为所需的值可以提供简洁(并且在您的情况下使用 atoi() 更强大的解决方案。

在这种情况下,使用精心制作的 格式字符串 可以轻松分隔您的字段。例如,将每一行读入缓冲区(在本例中为 buf),您可以将每一行分隔为所需的值:

        if (sscanf (buf, "%d;%c;%15[^;];%c;%d;%c",      /* convert to person/VALIDATE */
                    &person[n].id, &person[n].key, person[n].name,
                    &person[n].rel, &person[n].age, &person[n].status) == 6)

sscanf()int 的转换至少最低限度地验证了整数转换。 atoi() 并非如此,它会很乐意接受 atoi ("my cow") 并在没有任何迹象表明出现问题的情况下静默返回零。

请注意,在每次转换为字符串时,您必须提供一个 field-width 修饰符以将存储的字符数限制为比您的数组可以容纳的字符数少一个(为'[=23=]' 空终止符)。否则使用 scanf() 系列 "%s""%[..]" 并不比使用 gets() 更安全。参见 Why gets() is so dangerous it should never be used!

person[] 的数组边界保护同样适用于您的读取循环。只需在下一次读取之前记录成功的转换和测试即可,例如

#define NPERSONS  12        /* if you need a constant, #define one (or more) */
#define MAXNAME   16
#define MAXC    1024
...
    char buf[MAXC];                                     /* buffer to hold each line */
    size_t n = 0;                                       /* person counter/index */
    Person person[NPERSONS] = {{ .id = 0 }};            /* initialize all elements */
    /* use filename provided as 1st argument (stdin by default) */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
    ...
    while (n < NPERSONS && fgets (buf, MAXC, fp)) {     /* protect array, read line   */
        if (sscanf (buf, "%d;%c;%15[^;];%c;%d;%c",      /* convert to person/VALIDATE */
                    &person[n].id, &person[n].key, person[n].name,
                    &person[n].rel, &person[n].age, &person[n].status) == 6)
            n++;        /* increment count on good conversion */
    }

如上面的 #define 所示,不要在代码中使用 MagicNumbers。 (例如 1216)。相反,在您的代码顶部声明一个常量,如果您的限制以后需要调整,它提供了一个方便的单一位置来更改。

同样,不要硬编码文件名。没有理由仅仅为了从不同的文件中读取就必须重新编译代码。将文件名作为第一个参数传递给您的程序(这就是 argcargv 的作用),或者提示用户并将文件名作为输入。上面的代码将文件名作为第一个参数,如果没有提供参数,则默认从 stdin 读取(就像大多数 Unix 实用程序一样)。

总而言之,您可以执行类似于以下操作的操作:

#include <stdio.h>

#define NPERSONS  12        /* if you need a constant, #define one (or more) */
#define MAXNAME   16
#define MAXC    1024

typedef struct Person {
    int id;
    char key;
    char name[MAXNAME];
    char rel;
    int age;
    char status;
} Person;

int main (int argc, char **argv) {

    char buf[MAXC];                                     /* buffer to hold each line */
    size_t n = 0;                                       /* person counter/index */
    Person person[NPERSONS] = {{ .id = 0 }};            /* initialize all elements */
    /* 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 (n < NPERSONS && fgets (buf, MAXC, fp)) {     /* protect array, read line   */
        if (sscanf (buf, "%d;%c;%15[^;];%c;%d;%c",      /* convert to person/VALIDATE */
                    &person[n].id, &person[n].key, person[n].name,
                    &person[n].rel, &person[n].age, &person[n].status) == 6)
            n++;        /* increment count on good conversion */
    }
    if (fp != stdin)   /* close file if not stdin */
        fclose (fp);
    
    for (size_t i = 0; i < n; i++)                      /* output results */
        printf ("person[%zu]  %3d  %c  %-15s  %c  %3d  %c\n", i,
                person[i].id, person[i].key, person[i].name,
                person[i].rel, person[i].age, person[i].status);
}

(注意: 您只需要调用一次 printf() 即可输出带有转换的任何连续输出块。如果不需要转换,请使用 puts()fputs() 如果需要行尾控制)

最后,不要吝啬缓冲区大小16 对于 name 字段来说似乎太短了(64 仍在推动它)。通过使用 field-width 修饰符,你可以防止 Undefined Behavior 由于覆盖你的数组边界(代码将简单地跳过该行),但是在这种情况下,您应该添加一个 else { ... } 条件来输出错误。 16 足以满足您的示例数据,但对于一般用途,您需要将其调整为更大的值。

例子Use/Output

在名为 dat/person_id-status.txt 的文件中输入示例,您可以:

$ ./bin/person_id-status dat/person_id-status.txt
person[0]    1  A  John Mott        D   30  Z
person[1]    2  B  Judy Moor        S   60  X
person[2]    3  A  Kae Blanchett    S   42  y
person[3]    4  B  Jair Tade        S   21  W

这些是让我查看您的代码的要点。 (我敢肯定我忘了再提一两个)检查一下,如果您还有其他问题,请告诉我。