我想在每一行中读取一个包含电影信息的 txt,并将其保存到一个动态结构数组中

I want to read a txt with infos of a movie in each line and save it in to a dynamic array of structures

我真的是 C 编程的新手,我尝试将其作为读取文件并将它们保存到动态结构数组的示例,txt 的信息是:

Movie id:1448
title:The movie
surname of director: lorez
name of director: john
date: 3
month: september
year: 1997

结构应该是这样的

typedef struct date
{
  int day, month, year;
} date;

typedef struct director_info
{
  char* director_surname, director_name;
} director_info;

typedef struct movie
{
  int id;
  char* title;
  director_info* director;
  date* release_date;
} movie;

我只知道我应该用 fgets 阅读它,我认为这是某种方式,但我不知道如何制作结构并保存它们

    FILE *readingText;
    readingText = fopen("movies.txt", "r");

    if (readingText == NULL)
    {
        printf("Can't open file\n");
        return 1;
    }

    while (fgets(line, sizeof(line), readingText) != NULL)
    {
        .....
    }
    fclose(readingText);

读取 multi-line 输入可能有点挑战,将其与嵌套结构的分配相结合,您对文件 I/O 和动态内存分配有很好的学习经验。但在查看您的任务之前,有一些误解需要清理:

char* director_surname, director_name;

不声明两个 pointers-to char。它声明了一个指针 (director_surname),然后是一个字符 (director_name)。课程,指示指针间接级别的一元 '*' 与变量而不是类型一起使用。为什么?正如您所经历的那样:

char* a, b, c;

不声明三个pointers-to char,它声明一个指针和两个char变量。使用:

char *a, b, c;

说的很清楚。

Multi-Line阅读

当您必须协调来自多行的数据时,在您认为该组的输入有效之前,您必须验证您为该组中的每一行获取了所需的信息。有许多方法,但可能更多 straight-forward 之一是简单地使用临时变量来保存每个输入,并保留一个计数器,每次收到成功的输入时都会递增。如果您填充了所有临时变量,并且您的计数器反映了正确的输入数量,那么您可以为每个结构分配内存并将临时变量复制到它们的永久存储中。然后,您将计数器重置为零,并重复直到 运行 超出文件中的行数。

您的大部分读取都是 straight-forward,但 month 除外,它被读取为给定月份的 lower-case 字符串,然后您必须将其转换为 int 用于存储在您的 struct date 中。可能最简单的处理方法是创建一个 lookup-table(例如,十二个月中每个月的指向 string-literals 的常量指针数组)。然后在读取您的月份字符串后,您可以使用 strcmp() 循环遍历数组,以将该月份的索引映射到您的结构。 (添加 +1 来制作,例如 january1february2 等...)例如,您可以使用如下内容:

const char *months[] = { "january", "february", "march", "april",
                        "may", "june", "july", "august", "september",
                        "october", "november", "december" };
#define NMONTHS (int)(sizeof months / sizeof *months)

其中宏NMONTHS对于months中的元素个数是12

然后为了读取您的文件,您的基本方法是用 fgets() 读取每一行,然后从 sscanf() [=229= 的行中解析所需的信息]validating 沿途的每一次输入、转换和分配。 验证 是任何成功代码的关键,对于 multi-line 读取和转换尤其重要。

例如给定你的结构,你可以声明你额外需要的常量并声明和初始化你的临时变量,并打开作为第一个参数给出的文件和 validate 开放阅读:

...
#define MAXC 1024       /* if you need a constant, #define one (or more) */
#define MAXN  128
#define AVAIL   2
...
int main (int argc, char **argv) {
    
    char line[MAXC], tmptitle[MAXN], tmpsurnm[MAXN], tmpnm[MAXN], tmpmo[MAXN];
    int good = 0, tmpid;
    date tmpdt = { .day = 0 };      /* temporary date struct to fill */
    movie *movies = NULL;
    size_t avail = AVAIL, used = 0;
    /* 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;
    }

在您的 good 变量之上将是您的计数器,您会在每次正确读取和转换来自构成输入块的七行数据中的每一行数据时递增。当 good == 7 您将确认您拥有与一部电影相关的所有数据,并且您可以分配并使用所有临时值填充最终存储。

usedavail 计数器跟踪分配的 struct movie 有多少可用,其中有多少已使用。当 used == avail 时,您知道是时候 realloc() 您的电影块添加更多了。这就是动态分配方案的工作原理。您分配了一些您需要的预期数量的 object。你循环读取和填充 object 直到你填满你分配的内容,然后你重新分配更多并继续前进。

您可以根据需要每次添加任意多的额外内存,但一般方案是每次需要重新分配时将分配的内存加倍。这在所需的分配数量和可用 object 数量的增长之间提供了良好的平衡。

(内存操作相对昂贵,您希望避免为每个新的外部结构分配——尽管分配在扩展方面比每次创建新结构和复制要好一些,但使用分配更大块的方案将最后仍然是一种更有效的方法)

现在声明了临时变量和计数器,您可以开始 multi-line 读取。我们以第id行为例:

    while (fgets (line, MAXC, fp)) {    /* read each line */
        /* read ID line & validate conversion */
        if (good == 0 && sscanf (line, "Movie id: %d", &tmpid) == 1)
            good++;     /* increment good line counter */

您阅读该行并检查 good == 0 是否与 id 行协调阅读。您尝试转换为 int 并验证两者。如果你成功地在你的临时 ID 中存储了一个整数,你就会增加你的 good 计数器。

您对标题行的阅读将是相似的,只是这次它将是 else if 而不是普通的 if。上面的 id 行和从下一行读取的 title 将是:

     while (fgets (line, MAXC, fp)) {    /* read each line */
        /* read ID line & validate conversion */
        if (good == 0 && sscanf (line, "Movie id: %d", &tmpid) == 1)
            good++;     /* increment good line counter */
        /* read Title line & validate converion */
        else if (good == 1 && sscanf (line, "title:%127[^\n]", tmptitle) == 1)
            good++;     /* increment good line counter */

(注意: 任何时候你用任何 scanf() 函数族将字符串读入任何数组,你 必须 使用 field-width 修饰符127 以上)将读取限制为您的数组可以容纳的内容('[=65=]' 为 +1)以保护您的数组边界不被覆盖。如果您没有包含 field-width 修饰符,那么使用 scanf() 函数并不比使用 gets() 更安全。参见:Why gets() is so dangerous it should never be used!)

读取每一行并成功转换和存储后,good 将递增以设置将下一行的值读取到适当的临时变量中。

请注意,我说过您在 month 读取和由于读取而进行的转换方面还有更多工作要做,例如"september",但需要在结构中存储整数 9。从一开始就使用 lookup-table ,您将读取并获取月份名称的字符串,然后循环查找 lookup-table 中的索引(您需要将 +1 添加到索引中所以 january == 1,依此类推)。你可以这样做:

        /* read Month line and loop comparing with array to map index */
        else if (good == 5 && sscanf (line, "month: %s", tmpmo) == 1) {
            tmpdt.month = -1;   /* set month -1 as flag to test if tmpmo found */
            for (int i = 0; i < NMONTHS; i++) {
                if (strcmp (tmpmo, months[i]) == 0) {
                    tmpdt.month = i + 1;    /* add 1 to make january == 1, etc... */
                    break;
                }
            }
            if (tmpdt.month > -1)   /* if not found, flag still -1 - failed */
                good++;
            else
                good = 0;
        }

year 的最后一个 else if 之后,您添加了一个 else,这样块中任何一行的任何故障都将重置 good = 0;,因此它将尝试读取并匹配文件中的下 id 行,例如

        /* read Year line & validate */
        else if (good == 6 && sscanf (line, "year: %d", &tmpdt.year) == 1)
            good++;
        else
            good = 0;

动态分配

嵌套结构的动态分配并不难,但您必须清楚如何处理它。您的外部结构 struct movie 是您将使用 used == avail 等分配和重新分配的结构......每次您都必须分配 struct datestruct director_info您的七个临时变量已填充并验证并准备好放入最终存储中。您将通过检查 struct movie 块是否已分配来启动您的分配块,如果没有分配的话。如果有,并且 used == avail,您重新分配它。

现在每次 realloc() 使用 临时指针 ,所以当(不是如果)realloc() 返回 NULL 失败时,您不要丢失指向当前分配的存储空间的指针,方法是用返回的 NULL 覆盖它——创建一个 memory-leak。为您的 struct movie 分配或重新分配的初始处理看起来像:

        /* if good 7, all sequential lines and values for movie read */
        if (good == 7) {
            director_info *newinfo;     /* declare new member pointers */
            date *newdate;
            size_t len;
            
            /* if 1st allocation for movies, allocate AVAIL no. of movie struct */
            if (movies == NULL) {
                movies = malloc (avail * sizeof *movies);
                if (!movies) {                  /* validate EVERY allocation */
                    perror ("malloc-movies");
                    exit (EXIT_FAILURE);
                }
            }
            /* if movies needs reallocation */
            if (used == avail) {
                /* ALWAYS realloc with a temporary pointer */
                void *tmp = realloc (movies, 2 * avail * sizeof *movies);
                if (!tmp) {
                    perror ("realloc-movies");
                    break;
                }
                movies = tmp;
                avail *= 2;
            }

现在你有一个有效的 struct movie 块,你可以在其中直接存储 id 并为 title 分配并将分配的块分配给你的 [=62] =] 每个 struct movie 存储空间中的指针。我们首先分配两个 struct movie。当您启动 used == 0avail = 2 时(请参阅顶部的 AVAIL 常量了解 2 的来源)。处理 id 并为 title 分配将工作为:

            movies[used].id = tmpid;    /* set movie ID to tmpid */
            
            /* get length of tmptitle, allocate, copy to movie title */
            len = strlen (tmptitle);
            if (!(movies[used].title = malloc (len + 1))) {
                perror ("malloc-movies[used].title");
                break;
            }
            memcpy (movies[used].title, tmptitle, len + 1);

(注意:当你在一个内存块中声明多个结构并使用[..]索引每个结构时,[..]充当指针的解引用,所以你使用'.' 运算符访问 [..] 之后的成员,而不是 '->' 运算符,因为您通常会取消引用结构指针以访问成员(取消引用已经由 [..] 完成)

此外,既然你知道 len,就没有理由使用 strcpy()tmptitle 复制到 movies[used].title 并让 strcpy() 扫描在末尾寻找 nul-terminating 字符的字符串。您已经知道字符数,因此只需使用 memcpy() 复制 len + 1 个字节。 (请注意,如果您有 strdup(),您可以在 single-call 中分配和复制,但请注意 strdup() 不是 C11 中的 c 库的一部分。

每个 struct movie 元素的 struct director_info 分配是 straight-forward。您分配 struct director_info 然后使用 strlen() 获取名称的长度,然后像我们上面那样为每个和 memcpy() 分配存储空间。

            /* allocate director_info struct & validate */
            if (!(newinfo = malloc (sizeof *newinfo))) {
                perror ("malloc-newinfo");
                break;
            }
            
            len = strlen (tmpsurnm);    /* get length of surname, allocate, copy */
            if (!(newinfo->director_surname = malloc (len + 1))) {
                perror ("malloc-newinfo->director_surname");
                break;
            }
            memcpy (newinfo->director_surname, tmpsurnm, len + 1);
            
            len = strlen (tmpnm);       /* get length of name, allocate, copy */
            if (!(newinfo->director_name = malloc (len + 1))) {
                perror ("malloc-newinfo->director_name");
                break;
            }
            memcpy (newinfo->director_name, tmpnm, len + 1);
            
            movies[used].director = newinfo;    /* assign allocated struct as member */

处理分配和填充新的 struct date 更加容易。您只需分配并分配 3 个整数值,然后将分配的 struct date 的地址分配给 struct movie 中的指针,例如

            /* allocate new date struct & validate */
            if (!(newdate = malloc (sizeof *newdate))) {
                perror ("malloc-newdate");
                break;
            }
            
            newdate->day = tmpdt.day;       /* populate date struct from tmpdt struct */
            newdate->month = tmpdt.month;
            newdate->year = tmpdt.year;
                    
            movies[used++].release_date = newdate;  /* assign newdate as member */
            good = 0;
        }

就是这样,当您分配 struct movie 中的最后一个指针时,您会递增 used++,这样您就可以设置为用文件中接下来的七行填充该块中的下一个元素。您重置 good = 0; 以准备读取循环以读取文件中的下 id 行。

综合

如果您将代码全部填入,您最终会得到类似于:

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

#define MAXC 1024       /* if you need a constant, #define one (or more) */
#define MAXN  128
#define AVAIL   2

const char *months[] = { "january", "february", "march", "april",
                        "may", "june", "july", "august", "september",
                        "october", "november", "december" };
#define NMONTHS (int)(sizeof months / sizeof *months)

typedef struct date {
      int day, month, year;
} date;

typedef struct director_info {
      char *director_surname, *director_name;
} director_info;

typedef struct movie {
  int id;
  char *title;
  director_info *director;
  date *release_date;
} movie;

void prnmovies (movie *movies, size_t n)
{
    for (size_t i = 0; i < n; i++)
        printf ("\nMovie ID : %4d\n"
                "Title    : %s\n"
                "Director : %s %s\n"
                "Released : %02d/%02d/%4d\n",
                movies[i].id, movies[i].title, 
                movies[i].director->director_name, movies[i].director->director_surname,
                movies[i].release_date->day, movies[i].release_date->month,
                movies[i].release_date->year);
}

void freemovies (movie *movies, size_t n)
{
    for (size_t i = 0; i < n; i++) {
        free (movies[i].title);
        free (movies[i].director->director_surname);
        free (movies[i].director->director_name);
        free (movies[i].director);
        free (movies[i].release_date);
    }
    free (movies);
}

int main (int argc, char **argv) {
    
    char line[MAXC], tmptitle[MAXN], tmpsurnm[MAXN], tmpnm[MAXN], tmpmo[MAXN];
    int good = 0, tmpid;
    date tmpdt = { .day = 0 };      /* temporary date struct to fill */
    movie *movies = NULL;
    size_t avail = AVAIL, used = 0;
    /* 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 (fgets (line, MAXC, fp)) {    /* read each line */
        /* read ID line & validate conversion */
        if (good == 0 && sscanf (line, "Movie id: %d", &tmpid) == 1)
            good++;     /* increment good line counter */
        /* read Title line & validate converion */
        else if (good == 1 && sscanf (line, "title:%127[^\n]", tmptitle) == 1)
            good++;     /* increment good line counter */
        /* read director Surname line & validate */
        else if (good == 2 && sscanf (line, "surname of director: %127[^\n]", 
                                        tmpsurnm) == 1)
            good++;
        /* read directory Name line & validate */
        else if (good == 3 && sscanf (line, "name of director: %127[^\n]", tmpnm) == 1)
            good++;
        /* read Day line & validate */
        else if (good == 4 && sscanf (line, "date: %d", &tmpdt.day) == 1)
            good++;
        /* read Month line and loop comparing with array to map index */
        else if (good == 5 && sscanf (line, "month: %s", tmpmo) == 1) {
            tmpdt.month = -1;   /* set month -1 as flag to test if tmpmo found */
            for (int i = 0; i < NMONTHS; i++) {
                if (strcmp (tmpmo, months[i]) == 0) {
                    tmpdt.month = i + 1;    /* add 1 to make january == 1, etc... */
                    break;
                }
            }
            if (tmpdt.month > -1)   /* if not found, flag still -1 - failed */
                good++;
            else
                good = 0;
        }
        /* read Year line & validate */
        else if (good == 6 && sscanf (line, "year: %d", &tmpdt.year) == 1)
            good++;
        else
            good = 0;
        
        /* if good 7, all sequential lines and values for movie read */
        if (good == 7) {
            director_info *newinfo;     /* declare new member pointers */
            date *newdate;
            size_t len;
            
            /* if 1st allocation for movies, allocate AVAIL no. of movie struct */
            if (movies == NULL) {
                movies = malloc (avail * sizeof *movies);
                if (!movies) {                  /* validate EVERY allocation */
                    perror ("malloc-movies");
                    exit (EXIT_FAILURE);
                }
            }
            /* if movies needs reallocation */
            if (used == avail) {
                /* ALWAYS realloc with a temporary pointer */
                void *tmp = realloc (movies, 2 * avail * sizeof *movies);
                if (!tmp) {
                    perror ("realloc-movies");
                    break;
                }
                movies = tmp;
                avail *= 2;
            }
            
            movies[used].id = tmpid;    /* set movie ID to tmpid */
            
            /* get length of tmptitle, allocate, copy to movie title */
            len = strlen (tmptitle);
            if (!(movies[used].title = malloc (len + 1))) {
                perror ("malloc-movies[used].title");
                break;
            }
            memcpy (movies[used].title, tmptitle, len + 1);
            
            
            /* allocate director_info struct & validate */
            if (!(newinfo = malloc (sizeof *newinfo))) {
                perror ("malloc-newinfo");
                break;
            }
            
            len = strlen (tmpsurnm);    /* get length of surname, allocate, copy */
            if (!(newinfo->director_surname = malloc (len + 1))) {
                perror ("malloc-newinfo->director_surname");
                break;
            }
            memcpy (newinfo->director_surname, tmpsurnm, len + 1);
            
            len = strlen (tmpnm);       /* get length of name, allocate, copy */
            if (!(newinfo->director_name = malloc (len + 1))) {
                perror ("malloc-newinfo->director_name");
                break;
            }
            memcpy (newinfo->director_name, tmpnm, len + 1);
            
            movies[used].director = newinfo;    /* assign allocated struct as member */
            
            /* allocate new date struct & validate */
            if (!(newdate = malloc (sizeof *newdate))) {
                perror ("malloc-newdate");
                break;
            }
            
            newdate->day = tmpdt.day;       /* populate date struct from tmpdt struct */
            newdate->month = tmpdt.month;
            newdate->year = tmpdt.year;
                    
            movies[used++].release_date = newdate;  /* assign newdate as member */
            good = 0;
        }
        
    }
    if (fp != stdin)   /* close file if not stdin */
        fclose (fp);

    prnmovies (movies, used);       /* print stored movies */
    freemovies (movies, used);      /* free all allocated memory */
}

(注意:添加 prnmovies() 以输出所有存储的电影,添加 freemovies() 以释放所有分配的内存)

示例输入文件

不是一部电影只有一个七行的块,让我们添加另一个以确保代码将循环遍历文件,例如

$ cat dat/moviegroups.txt
Movie id:1448
title:The movie
surname of director: lorez
name of director: john
date: 3
month: september
year: 1997
Movie id:1451
title:Election - Is the Cheeto Tossed?
surname of director: loreza
name of director: jill
date: 3
month: november
year: 2020

示例Use/Output

处理文件名中包含两部电影数据的输入文件 dat/moviegroups.txt 你会:

$ ./bin/movieinfo dat/moviegroups.txt

Movie ID : 1448
Title    : The movie
Director : john lorez
Released : 03/09/1997

Movie ID : 1451
Title    : Election - Is the Cheeto Tossed?
Director : jill loreza
Released : 03/11/2020

内存Use/Error检查

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

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

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

$ valgrind ./bin/movieinfo dat/moviegroups.txt
==9568== Memcheck, a memory error detector
==9568== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==9568== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==9568== Command: ./bin/movieinfo dat/moviegroups.txt
==9568==

Movie ID : 1448
Title    : The movie
Director : john lorez
Released : 03/08/1997

Movie ID : 1451
Title    : Election - Is the Cheeto Tossed?
Director : jill loreza
Released : 03/10/2020
==9568==
==9568== HEAP SUMMARY:
==9568==     in use at exit: 0 bytes in 0 blocks
==9568==   total heap usage: 14 allocs, 14 frees, 5,858 bytes allocated
==9568==
==9568== All heap blocks were freed -- no leaks are possible
==9568==
==9568== For counts of detected and suppressed errors, rerun with: -v
==9568== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

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

这个答案中有很多信息(结果总是比我预期的要长),但是要对正在发生的事情给出一个公平的解释需要一点时间。慢慢来,理解每一段代码在做什么,理解分配是如何处理的(这需要时间来消化)。如果您遇到困难,请发表评论,我很乐意进一步解释。