使用带有 fscanf() 的 strtok() 标记进行日期分配

using a strtok() token with fscanf() to do a date assignment

我有一个名为 TEAM 的结构,其中包含有关足球队的一些信息。

    typedef struct team{
    int     ID;                 //1
    char    Team_name[MAX_LEN]; //2
    int     Status;             //3
    int     Points;             //4
    int     Score;              //5
    int     Goals;              //6
    struct tm  Date;            //7
    struct team *next;
}TEAM;

我正在创建团队链接列表,团队信息是从文件中读取的,格式如下:

ID;teamname;status;points;score;goals;day/month/year hour:minute

我从文件中获取数据并完成所需的作业没有问题。但是我对日期部分有疑问。

我使用 strtok() 将一行拆分为标记,并尝试使用以下代码将标记的各个部分分配给它们各自的变量。

fscanf(token, "%d/%d/%d %d:%d", &tmp->Date.tm_mday, &tmp->Date.tm_mon, &tmp->Date.tm_year, &tmp->Date.tm_hour, &tmp->Date.tm_min);

但这给出了这个 return 值 -1073741819 (0xC0000005) 据我所知,这被称为非法内存访问,我用“&”符号或 fscanf() 所做的事情是错误的。

我的代码在没有 fscanf() 的情况下工作正常所以我做错了什么?

编辑:整个代码:

typedef struct team{
    int     ID;                 //1
    char    Team_name[MAX_LEN]; //2
    int     Status;             //3
    int     Points;             //4
    int     Score;              //5
    int     Goals;              //6
    struct tm  Date;            //7
    struct team *next;
}TEAM;

TEAM *createNode(void){
    TEAM *node;
    node = (TEAM *)malloc(sizeof(TEAM));
    node->next = NULL;
    return node;
}

TEAM *initialiseTeams(void){
    FILE    *fp;
    TEAM    *head;
    TEAM    *tmp;
    char    line[BUFFER];
    char    *token;
    const char delim[2] = ";";

    fp = fopen("Teams.txt","r");

    if(fp == NULL){
        printf("Failed to open Teams.txt\n");
        exit(EXIT_FAILURE);
    }

    head = createNode();
    tmp  = head;
    while(fgets(line, BUFFER, fp)){

        int i;

        for(i = 0, token = strtok(line, delim); token != NULL; i++, token = strtok(NULL, delim)){
            printf("%s\n", token);
            switch(i){
                case 0: // ID
                    tmp->ID = atoi(token);
                    break;
                case 1: // Team name
                    strcpy(tmp->Team_name, token);
                    break;
                case 2: // Status
                    if(strcmp(token, "L") == 0){
                        tmp->Status = 0;
                    }

                    if(strcmp(token, "W") == 0){
                        tmp->Status = 1;
                    }

                    if(strcmp(token, "D") == 0){
                        tmp->Status = 2;
                    }
                    break;
                case 3: // Points
                    tmp->Points = atoi(token);
                    break;
                case 4: // Score
                    tmp->Score = atoi(token);
                    break;
                case 5: // Goals
                    tmp->Goals = atoi(token);
                    break;
                case 6: // Date
                    fscanf(token, "%d/%d/%d %d:%d", &tmp->Date.tm_mday, &tmp->Date.tm_mon, &tmp->Date.tm_year, &tmp->Date.tm_hour, &tmp->Date.tm_min);
                    break;
            }
        }
        tmp->next   = createNode();
        tmp         = tmp->next;
    }

    free(tmp);
    return head;
}

int main(void){
    TEAM    *head;
    head = initialiseTeams();
}

fscanfFILE * 作为其第一个参数,但您将代码描述为通过 token 以某种方式来自 strtok 。你没有说 token 是什么,但是没有办法从 strtok 得到一个 FILE *,所以它可能不是那个。

也许你想打电话给 sscanf

您创建列表的过程过于复杂。这部分是由于试图将所有 I/O 和列表操作卸载到 initialiseTeams()。通常,您希望将列表操作与程序界面(I/O、它与用户的交互方式等)分开。这样,无论您需要从何处添加节点(文件、用户-输入等),您只需调用 add()(或 push())函数即可将新节点添加到您的列表中。

你把 createnode() 函数分离得很好。您可以通过将指向 TEAM 结构的指针作为参数传递来进一步利用它,该结构可用于在 createnode() 函数中初始化新节点。例如:

TEAM *createNode (TEAM *t)
{
    TEAM *node = malloc (sizeof *node);   /* allocate */
    
    if (!node) {                          /* validate */
        perror ("malloc-node");
        return NULL;
    }
    
    if (t)                                /* assign struct (if not NULL) */
      *node = *t;
    node->next = NULL;
    
    return node;
}

只需很少的额外努力。在验证要添加的数据之前没有理由调用 createnode() 函数,因此传递 TEAM* 指针允许您处理函数中的所有创建和初始化。

您的 initialiseTeams() 应该将打开的 FILE* 流作为参数读取。如果文件无法打开并在调用者中返回验证——则无需首先调用 initialiseTeams() 函数。

您可以像阅读一样用fgets()阅读,但不需要用strtok()标记行。一个简单的精心制作的 sscanf() 格式字符串就可以了。这极大地简化了您的功能,将实现减少到:

/* read all records from fp, return pointer to beginning of list */
TEAM *initialiseTeams (FILE *fp)
{
    char line[BUFFER];
    TEAM *head = NULL,      /* using both head & tail pointer allows O(1) in-order */
         *tail = NULL;      /* insertions to the list. */
    
    while (fgets (line, BUFFER, fp)) {          /* read each line */
        TEAM tmp = { .ID = 0 };                 /* temporary struct */
        /* parse values with sscanf() validating return */
        if (sscanf (line, "%d; %127[^;]; %d; %d; %d; %d; %d/%d/%d %d:%d",
                    &tmp.ID, tmp.Team_name, &tmp.Status, &tmp.Points,
                    &tmp.Score, &tmp.Goals, &tmp.Date.tm_mday, &tmp.Date.tm_mon, 
                    &tmp.Date.tm_year, &tmp.Date.tm_hour, &tmp.Date.tm_min) == 11) {
            
            TEAM *node;                         /* node to allocate */
            if (!(node = createNode (&tmp)))    /* create node passing tmp struct */
                break;
            
            tmp.Date.tm_year -= 1900;           /* subtract 1900 from year */
            *node = tmp;                        /* assign tmp to allocated node */
            
            if (!head)                          /* if head NULL, add 1st node */
                head = tail = node;
            else {
                tail->next = node;              /* add all subsequent nodes in-order */
                tail = node;
            }
        }
    }
    
    return head;    /* return pointer to beginning of list */
}

(注意: 您需要将 "%127[^;]" 中的 field-width 修饰符调整为 MAX_LEN - 1)

切勿对文件名进行硬编码或在代码中使用 MagicNumbers。您不必为了读取不同的文件名而重新编译您的程序。要么将文件名的名称作为参数提供给您的程序(这就是 int argc, char **argvint main (int argc, char **argv) 中的作用,要么提示用户输入文件名。因为您显示 MAX_LENBUFFER,这些显然在其他地方被声明为常量——这很好。

将要读取的文件名作为程序的第一个参数,或者如果没有提供参数则默认从 stdin 读取,读取数据文件的程序可能类似于:

int main (int argc, char **argv) {
    
    TEAM    *head = NULL;   /* initialize pointers NULL */
    /*
     * keep interface separate from implementation.
     * open file/validate and pass open FILE* to initialiseTeams()
     * 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;
    }
    
    head = initialiseTeams (fp);    /* read all records in file into list */
    
    if (fp != stdin)    /* close file if not stdin */
        fclose (fp);
    
    prn_list (head);    /* print list */
    del_list (head);    /* free all nodes */
}

把它放在一起,快速编写 prn_list()del_list() 函数来输出列表,然后在完成后删除列表中的所有节点,你可以这样做:

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

#define MAX_LEN 128
#define BUFFER 1024

typedef struct team {
    int  ID;
    char Team_name[MAX_LEN];
    int  Status;
    int  Points;
    int  Score;
    int  Goals;
    struct tm Date;
    struct team *next;
} TEAM;

TEAM *createNode (TEAM *t)
{
    TEAM *node = malloc (sizeof *node);   /* allocate */
    
    if (!node) {                          /* validate */
        perror ("malloc-node");
        return NULL;
    }
    
    if (t)                                /* assign struct (if not NULL) */
      *node = *t;
    node->next = NULL;
    
    return node;
}

/* read all records from fp, return pointer to beginning of list */
TEAM *initialiseTeams (FILE *fp)
{
    char line[BUFFER];
    TEAM *head = NULL,      /* using both head & tail pointer allows O(1) in-order */
         *tail = NULL;      /* insertions to the list. */
    
    while (fgets (line, BUFFER, fp)) {          /* read each line */
        TEAM tmp = { .ID = 0 };                 /* temporary struct */
        /* parse values with sscanf() validating return */
        if (sscanf (line, "%d; %127[^;]; %d; %d; %d; %d; %d/%d/%d %d:%d",
                    &tmp.ID, tmp.Team_name, &tmp.Status, &tmp.Points,
                    &tmp.Score, &tmp.Goals, &tmp.Date.tm_mday, &tmp.Date.tm_mon, 
                    &tmp.Date.tm_year, &tmp.Date.tm_hour, &tmp.Date.tm_min) == 11) {
            
            TEAM *node;                         /* node to allocate */
            if (!(node = createNode (&tmp)))    /* create node passing tmp struct */
                break;
            
            tmp.Date.tm_year -= 1900;           /* subtract 1900 from year */
            *node = tmp;                        /* assign tmp to allocated node */
            
            if (!head)                          /* if head NULL, add 1st node */
                head = tail = node;
            else {
                tail->next = node;              /* add all subsequent nodes in-order */
                tail = node;
            }
        }
    }
    
    return head;    /* return pointer to beginning of list */
}

/* simple print function */
void prn_list (TEAM *list)
{
    if (!list) {
        puts ("(list empty)");
        return;
    }
    
    for (TEAM *iter = list; iter; iter = iter->next)
        printf ("%4d  %s\n"
                "  Status: %d\n"
                "  Points: %d\n"
                "  Score : %d\n"
                "  Goals : %d\n"
                "  Date  : %d/%d/%d %d:%d\n\n",
                iter->ID, iter->Team_name, iter->Status, iter->Points, iter->Score,
                iter->Goals, iter->Date.tm_mday, iter->Date.tm_mon, 
                iter->Date.tm_year + 1900, iter->Date.tm_hour, iter->Date.tm_min);
}

/* function to delete all nodes in list */
void del_list (TEAM *list)
{
    while (list) {
        TEAM *victim = list;
        list = list->next;
        free (victim);
    }
}

int main (int argc, char **argv) {
    
    TEAM    *head = NULL;   /* initialize pointers NULL */
    /*
     * keep interface separate from implementation.
     * open file/validate and pass open FILE* to initialiseTeams()
     * 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;
    }
    
    head = initialiseTeams (fp);    /* read all records in file into list */
    
    if (fp != stdin)    /* close file if not stdin */
        fclose (fp);
    
    prn_list (head);    /* print list */
    del_list (head);    /* free all nodes */
}

正在使用符合以下格式的生成数据文件测试代码:

ID;teamname;status;points;score;goals;day/month/year hour:minute

示例输入文件

例如:

$ cat dat/soccerteams.txt
42;Team42;3;0;0;2;9/2/2020 1:5
97;Team97;6;42;2;2;18/3/2020 2:11
54;Team54;2;35;0;4;9/4/2020 3:17
49;Team49;10;0;2;4;18/5/2020 4:23
46;Team46;7;28;8;1;18/6/2020 5:29
98;Team98;7;0;4;3;27/1/2020 6:35
15;Team15;2;7;6;0;9/2/2020 7:41
8;Team8;8;7;4;3;27/3/2020 8:47
4;Team4;4;28;8;4;18/4/2020 9:53
51;Team51;12;14;6;1;9/5/2020 10:59

例子Use/Output

按如下方式调用程序会产生所示输出:

$ ./bin/ll_soccer dat/soccerteams.txt
  42  Team42
  Status: 3
  Points: 0
  Score : 0
  Goals : 2
  Date  : 9/2/2020 1:5

  97  Team97
  Status: 6
  Points: 42
  Score : 2
  Goals : 2
  Date  : 18/3/2020 2:11

  54  Team54
  Status: 2
  Points: 35
  Score : 0
  Goals : 4
  Date  : 9/4/2020 3:17

  49  Team49
  Status: 10
  Points: 0
  Score : 2
  Goals : 4
  Date  : 18/5/2020 4:23

  46  Team46
  Status: 7
  Points: 28
  Score : 8
  Goals : 1
  Date  : 18/6/2020 5:29

  98  Team98
  Status: 7
  Points: 0
  Score : 4
  Goals : 3
  Date  : 27/1/2020 6:35

  15  Team15
  Status: 2
  Points: 7
  Score : 6
  Goals : 0
  Date  : 9/2/2020 7:41

   8  Team8
  Status: 8
  Points: 7
  Score : 4
  Goals : 3
  Date  : 27/3/2020 8:47

   4  Team4
  Status: 4
  Points: 28
  Score : 8
  Goals : 4
  Date  : 18/4/2020 9:53

  51  Team51
  Status: 12
  Points: 14
  Score : 6
  Goals : 1
  Date  : 9/5/2020 10:59

当您将列表操作与程序的其余部分分开时,它会使测试每个单独的组件变得更加容易,并使您的列表代码可重用。如果你把所有东西都放在几个大函数中——那么你最终将不得不在未来为每个列表重写它们。保持函数小且易于测试。

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