首页 > 解决方案 > 从文件读取数据时出现分段错误

问题描述

我在用 C 语言从 CSV 文件解析数据时遇到分段错误错误。我相信在阅读最后一行 <person[i].status> 时会给出错误,就好像我评论代码运行完美的同一行一样。

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

感谢您的帮助!

标签: cimport-from-csv

解决方案


虽然您有一个很好的答案来解决您的问题,但您可能会使用to begin withstrtok()来过度复杂化您的代码。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)

转换为intbysscanf()至少最低限度地验证了整数转换。不是这样,atoi()它会很高兴地接受atoi ("my cow")并失败,默默地返回零,而没有任何迹象表明事情出了问题。

请注意,在每次转换为字符串时,您必须提供一个字段宽度修饰符,以将存储的字符数限制为比数组可以容纳的少一个(为'\0'nul 终止字符节省空间)。否则scanf()家庭使用"%s""%[..]"不比使用更安全gets()。请参阅为什么 gets() 如此危险,永远不应该使用它!

对数组边界的相同保护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)。相反,在您的代码顶部声明一个常量,如果您的限制以后需要调整,它提供了一个方便的单一位置来更改。

同样,不要硬编码文件名。没有理由为了从不同的文件中读取而必须重新编译代码。将文件名作为第一个参数传递给您的程序(这就是它的argc用途argv),或者提示用户并将文件名作为输入。上面,代码将文件名作为第一个参数,如果没有提供参数,则默认读取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似乎非常短(仍在推动它)。通过使用字段宽度修饰符,您可以防止由于覆盖数组边界而导致未定义行为(代码将简单地跳过该行),但您应该添加一个条件以在这种情况下输出错误。对于您的示例数据来说已经足够了,但对于一般用途,您可能希望将其调整为更大的值。name64else { ... }16

示例使用/输出

使用名为 的文件中的示例输入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

那些结构我查看您的代码的要点。(我确定我忘了再提一两个)看看事情,如果你还有其他问题,请告诉我。


推荐阅读