首页 > 解决方案 > 使用 fgets() 和 strtok() 读取文本文件以分隔行中的字符串,从而产生不需要的行为

问题描述

我正在尝试使用 fgets() 和 strtok() 读取具有以下格式的文本文件。

1082018 1200 79 Meeting with President
2012018 1200 79 Meet with John at cinema
2082018 1400 30 games with Alpha
3022018 1200 79 sports

我需要将第一个值与行的其余部分分开,例如:

key=21122019, val = 1200 79 Meeting with President

为此,我使用strchr()forvalstrtok()for key,但是,从文件读取时键值保持不变。我不明白为什么会发生这种情况,因为我在 while 循环内为 in_key 分配空间,并且每次都将其放置在不同索引的数组中。

我的代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 1000 // max number of lines to be read
#define VALLEN 100
#define MAXC 1024

#define ALLOCSIZE 1000 /*size of available space*/
static char allocbuf[ALLOCSIZE]; /* storage for alloc*/
static char *allocp = allocbuf; /* next free position*/

char *alloc(int n) { /* return a pointer to n characters*/
    if (allocbuf + ALLOCSIZE - allocp >= n) { /*it fits*/
        allocp += n;
        return allocp - n; /*old p*/
    } else /*not enough room*/
        return 0;
}

int main(int argc, char** argv) {
    FILE *inp_cal;
    inp_cal = fopen("calendar.txt", "r+");

    char buf[MAXC];
    char *line[1024];
    char *p_line;

    char *in_val_arr[100];
    char *in_key_arr[100];
    int count = 0;
    char delimiter[] = " ";

    if (inp_cal) {
        printf("Processing file...\n");
        while (fgets(buf, MAXC, inp_cal)) {
            p_line = malloc(strlen(buf) + 1); // malloced with size of buffer.
            char *in_val;
            char *in_key;

            strcpy(p_line, buf);    //used to create a copy of input buffer
            line[count] = p_line;

            /* separating the line based on the first space. The words after
             * the delimeter will be copied into in_val */
            char *copy = strchr(p_line, ' ');
            if (copy) {
                if ((in_val = alloc(strlen(line[count]) + 1)) == NULL) {
                    return -1;
                } else {
                    strcpy(in_val, copy + 1);
                    printf("arr: %s", in_val);
                    in_val_arr[count] = in_val;
                }
            } else
                printf("Could not find a space\n");

            /* We now need to get the first word from the input buffer*/
            if ((in_key = alloc(strlen(line[count]) + 1)) == NULL) {
                return -1;
            }
            else {
                in_key = strtok(buf, delimiter);
                printf("%s\n", in_key);
                in_key_arr[count] = in_key; // <-- Printed out well
                count++;
            }
        }
        for (int i = 0; i < count; ++i)
            printf("key=%s, val = %s", in_key_arr[i], in_val_arr[i]); //<-- in_key_arr[i] contains same values throughout, unlike above
        fclose(inp_cal);
    }
    return 0;
}

while循环输出(正确):

Processing file...
arr: 1200 79 Meeting with President
1082018
arr: 1200 79 Meet with John at cinema
2012018
arr: 1400 30 games with Alpha
2082018
arr: 1200 79 sports
3022018

for循环输出(不正确):

key=21122019, val = 1200 79 Meeting with President
key=21122019, val = 1200 79 Meet with John
key=21122019, val = 1400 30 games with Alpha
key=21122019, val = 1200 79 sports

关于如何改进以及为什么会发生这种情况的任何建议?谢谢

标签: carraysfilepointerstext

解决方案


继续评论,在尝试使用strtok将您的数据key, val, somenum和行的其余部分分隔为字符串时,您使事情变得比需要的更难。

如果您的行的开头始终是:

key val somenum rest

您可以简单地使用sscanf来解析key, valsomenum转换为例如三个unsigned值,然后将行的其余部分转换为字符串。key, val, somenum为了帮助保持每个和之间的关系string,将每一行的值存储在 astruct中非常容易跟踪所有内容。您甚至可以分配string以最小化存储到所需的确切数量。例如,您可以使用如下内容:

typedef struct {    /* struct to handle values */
    unsigned key, val, n;
    char *s;
} keyval_t;

然后在main()你可以分配一些初始数量的结构,保持索引作为计数器,使用临时结构和缓冲区循环读取每一行,然后分配字符串(+1用于nul 终止字符)并将值复制到你的结构. 当填充的结构数量达到您分配realloc的数量时,只需结构数量并继续。

例如,假设您最初为NSTRUCTstruts 分配并将每一行读入buf,例如

...
#define NSTRUCT    8    /* initial struct to allocate */
#define MAXC    1024    /* read buffer size (don't skimp) */
...
    /* allocate/validate storage for max struct */
    if (!(kv = malloc (max * sizeof *kv))) {
        perror ("malloc-kv");
        return 1;
    }
    ...
    size_t ndx = 0,         /* used */
        max = NSTRUCT;      /* allocated */
    keyval_t *kv = NULL;    /* ptr to struct */
    ...
    while (fgets (buf, MAXC, fp)) { /* read each line of input */
    ...

在您的while循环中,您只需要使用 解析值sscanf,例如

        char str[MAXC];
        size_t len;
        keyval_t tmp = {.key = 0};  /* temporary struct for parsing */
        if (sscanf (buf, "%u %u %u %1023[^\n]", &tmp.key, &tmp.val, &tmp.n,
            str) != 4) {
            fprintf (stderr, "error: invalid format, line '%zu'.\n", ndx);
            continue;
        }

解析值后,您检查索引是否已达到您分配的结构数量以及realloc是否需要(注意使用临时指针realloc),例如

        if (ndx == max) {    /* check if realloc needed */
            /* always realloc with temporary pointer */
            void *kvtmp = realloc (kv, 2 * max * sizeof *kv);
            if (!kvtmp) {
                perror ("realloc-kv");
                break;  /* don't exit, kv memory still valid */
            }
            kv = kvtmp; /* assign new block to pointer */
            max *= 2;   /* increment max allocated */
        }

现在有了 的存储struct,只需获取字符串的长度,将unsigned值复制到您的结构,然后为 分配length + 1字符kv[ndx].s并复制strkv[ndx].s,例如

        len = strlen(str);              /* get length of str */
        kv[ndx] = tmp;                  /* assign tmp values to kv[ndx] */
        kv[ndx].s = malloc (len + 1);   /* allocate block for str */
        if (!kv[ndx].s) {               /* validate */
            perror ("malloc-kv[ndx].s");
            break;  /* ditto */
        }
        memcpy (kv[ndx++].s, str, len + 1); /* copy str to kv[ndx].s */
    }

注意:如果你用strdup替换它,你可以使用malloc它,但由于分配,如果你走那条路线,不要忘记在递增之前检查)memcpykv[ndx].s = strdup (str);strdupkv[ndx].s != NULLndx

这几乎是捕获数据的简单而可靠的方法。它现在包含在分配的结构数组中,您可以根据需要使用它,例如

    for (size_t i = 0; i < ndx; i++) {
        printf ("kv[%2zu] : %8u  %4u  %2u  %s\n", i,
            kv[i].key, kv[i].val, kv[i].n, kv[i].s);
        free (kv[i].s);     /* free string */
    }

    free (kv);  /* free stucts */

(不要忘记释放你分配的内存)

总而言之,您可以执行以下操作:

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

#define NSTRUCT    8    /* initial struct to allocate */
#define MAXC    1024    /* read buffer size (don't skimp) */

typedef struct {    /* struct to handle values */
    unsigned key, val, n;
    char *s;
} keyval_t;

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

    char buf[MAXC];         /* line buffer */
    size_t ndx = 0,         /* used */
        max = NSTRUCT;      /* allocated */
    keyval_t *kv = NULL;    /* ptr to struct */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;

    if (!fp) {  /* validate file open for reading */
        perror ("fopen-file");
        return 1;
    }

    /* allocate/validate storage for max struct */
    if (!(kv = malloc (max * sizeof *kv))) {
        perror ("malloc-kv");
        return 1;
    }

    while (fgets (buf, MAXC, fp)) { /* read each line of input */
        char str[MAXC];
        size_t len;
        keyval_t tmp = {.key = 0};  /* temporary struct for parsing */
        if (sscanf (buf, "%u %u %u %1023[^\n]", &tmp.key, &tmp.val, &tmp.n,
            str) != 4) {
            fprintf (stderr, "error: invalid format, line '%zu'.\n", ndx);
            continue;
        }
        if (ndx == max) {    /* check if realloc needed */
            /* always realloc with temporary pointer */
            void *kvtmp = realloc (kv, 2 * max * sizeof *kv);
            if (!kvtmp) {
                perror ("realloc-kv");
                break;  /* don't exit, kv memory still valid */
            }
            kv = kvtmp; /* assign new block to pointer */
            max *= 2;   /* increment max allocated */
        }
        len = strlen(str);              /* get length of str */
        kv[ndx] = tmp;                  /* assign tmp values to kv[ndx] */
        kv[ndx].s = malloc (len + 1);   /* allocate block for str */
        if (!kv[ndx].s) {               /* validate */
            perror ("malloc-kv[ndx].s");
            break;  /* ditto */
        }
        memcpy (kv[ndx++].s, str, len + 1); /* copy str to kv[ndx].s */
    }

    if (fp != stdin)    /* close file if not stdin */
        fclose (fp);

    for (size_t i = 0; i < ndx; i++) {
        printf ("kv[%2zu] : %8u  %4u  %2u  %s\n", i,
            kv[i].key, kv[i].val, kv[i].n, kv[i].s);
        free (kv[i].s);     /* free string */
    }

    free (kv);  /* free stucts */
}

示例使用/输出

使用您的数据文件作为输入,您将收到以下信息:

$ ./bin/fgets_sscanf_keyval <dat/keyval.txt
kv[ 0] :  1082018  1200  79  Meeting with President
kv[ 1] :  2012018  1200  79  Meet with John at cinema
kv[ 2] :  2082018  1400  30  games with Alpha
kv[ 3] :  3022018  1200  79  sports

内存使用/错误检查

在您编写的任何动态分配内存的代码中,对于分配的任何内存块,您有两个责任:(1)始终保留指向内存块起始地址的指针,(2)它可以在没有时被释放更需要。

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

对于 Linuxvalgrind是正常的选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。

$ valgrind ./bin/fgets_sscanf_keyval <dat/keyval.txt
==6703== Memcheck, a memory error detector
==6703== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==6703== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==6703== Command: ./bin/fgets_sscanf_keyval
==6703==
kv[ 0] :  1082018  1200  79  Meeting with President
kv[ 1] :  2012018  1200  79  Meet with John at cinema
kv[ 2] :  2082018  1400  30  games with Alpha
kv[ 3] :  3022018  1200  79  sports
==6703==
==6703== HEAP SUMMARY:
==6703==     in use at exit: 0 bytes in 0 blocks
==6703==   total heap usage: 5 allocs, 5 frees, 264 bytes allocated
==6703==
==6703== All heap blocks were freed -- no leaks are possible
==6703==
==6703== For counts of detected and suppressed errors, rerun with: -v
==6703== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

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

如果您有任何进一步的问题,请检查一下并让我现在。如果需要进一步拆分kv[i].s,那么可以考虑使用strtok


推荐阅读