c - 在循环链接列表中存储用户输入的名称

标签 c arrays linked-list user-input

我目前正在尝试制作一个程序,在其中我可以输入信息(在这种情况下,信息将是名称/字符串),并将所述信息动态存储在循环链表中。我想要删除已经创建的节点,但是现在我只是停留在最初创建节点的过程中,之后再进行。我对此还比较陌生,因此概念对我来说有点抽象。我基本上在网上看到了一些代码,这些代码在我想做的事情之后都具有相同的概念,并试图弄清楚每个句子的作用,以便我更好地理解如何实现此目的,但是这样做仍然会出错
编译后一直出现的错误是“结构节点没有名为子级的成员”。但据我所知

这是下面的代码

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

#define SIZE 20 //char array size for names

struct node
{
char players[SIZE];
struct node *next;
}*firstnode;

void  createList(int amount);
void displayList();

int main()
{
char children[SIZE];
int amount; //stores the number of children that will be playing
firstnode = NULL;

printf(" Welcome To The Count Out Game!\n");    //Header
printf("------------------------------------\n\n");    //Header 

printf("How Many Children Will Be Playing? : ");
scanf("%d", &amount);

void createList(int amount)
    {
        int i;
        char children [SIZE];

        struct node *prevNode, *newNode;

        if(amount>=1)
            {
                firstnode = (struct node *)malloc(sizeof(struct node));

                printf("Enter The Name For Child 1: ");
                scanf("%s", &children);

                firstnode->children = children;
                firstnode->next= NULL;

                prevNode = firstnode;

                for(i=2; i<=amount; i++)
                    {
                        newNode = (struct node *)malloc(sizeof(struct node));
                        printf("Enter the name for child %d", i);
                        scanf("%s", &children);

                        newNode->children = children;
                        newNode->next = NULL;

                        prevNode->next = newNode;
                        prevNode = newNode;

                        prevNode->next = firstnode;
                    }
              }


    }

void displayList()
    {
        struct node *current;
        int n = 1;

        if(firstnode == NULL)
            {
                printf("List Is Empty");
            }

        else
            {
                current = firstnode;
                printf("Names Of Children In The List\n");

                do
                    {
                        printf("Names %s\n", n, current->firstnode);

                        current = current->next;
                        n++;
                    }
                while(current!= firstnode);
            }
    }
}

最佳答案

对于初学者来说,处理单数链接(循环)链表作为链表的介绍,比简单的Head-> Tail列表(其中last->next指针只是NULL)要花费更多的思考和理解。以非循环列表为例:

Singly Linked-List (non-circular)

             Node1                 Node2                 Node3
         +------------+        +------------+        +------------+
         |   Payload  |        |   Payload  |        |   Payload  |
         +------------+        +------------+        +------------+
         |   Next     |------->|   Next     |------->|   Next     |--->NULL
         +------------+        +------------+        +------------+ 


上面,简单地链接(将next指针设置为last->next时,只需通过NULL指针将节点钩在一起即可。这使得添加到列表变得微不足道,因为您可以简单地将新节点添加为新的第一个节点每次更改列表地址,例如

list *newnode = malloc (sizeof *newnode);  /* validate, set data values, ... */
newnode->next = list;
list = newnode;


或者您可以简单地迭代while (node->next != NULL)然后在末尾添加新节点,例如

node->next = newnode;
newnode->next = NULL;


非循环列表的优点是简单,但缺点是只能从列表的开头到结尾进行迭代。您不能从列表中的任何点从任何节点迭代回到该节点之前的那个节点。 (这对于大型列表可能会产生很大的效率差异)

要解决此问题,循环列表的last->next指针指向列表的开头。有了这一新增功能,您可以在整个列表iter = current;中从while (iter->next != current)进行迭代,从而可以从列表中的任何点迭代到列表中的任何其他点,而无需从头开始。这只会带来一点额外的复杂性。考虑一下:

 Singly Linked-List (circular)

             Node1                 Node2                 Node3
         +------------+        +------------+        +------------+
         |   Payload  |        |   Payload  |        |   Payload  |
         +------------+        +------------+        +------------+
    +--->|   Next     |------->|   Next     |------->|   Next     |--->+
    |    +------------+        +------------+        +------------+    |
    |                                                                  |
    +<--------------------<---------------------<----------------------+


现在,将节点添加到列表中时,有两种特殊情况可以确保您处理。例如,当添加第一节点时,由于列表是圆形的,因此第一节点是自参考的(或者,由于缺少更好的单词,所以是自参考的),例如。

Singly Linked-List (circular) - 1st Node is Self-Referencial

             Node1
         +------------+
         |   Payload  |
         +------------+
    +--->|   Next     |--->+
    |    +------------+    |
    |                      |
    +<---------------------+


这并不会增加很多复杂性,但是需要您对如何将节点添加到列表中并确保last->next指针始终指向列表的开头进行更深入的思考。这也需要在迭代列表时多加注意,因为您迭代直到current->next指针等于起点(通常是列表地址,但可以是任何节点)。

从循环列表开始学习列表是可以的,但是您必须始终保持指针直立。最好的方法是简单地拔出铅笔并画出正在处理的节点(就像我用作图表的方框一样),并且当需要添加节点时,请确保重新连接所有指针在列表中插入新节点时正确地进行。从列表中删除节点的方法相同。使用橡皮擦将其清除,然后对指针的重新布线进行编码,以再次将您的列表缝在一起。

您的代码通过不使用描述性的变量名称(至少混合了子代,播放器和子代,等等……在我看来不起作用)使事情变得比原本要难的事情。struct中的每个节点都将包含一个< cc>,而不是多个player。保持变量名的单数和复数形式很长,这有助于使逻辑保持直线。重命名,例如

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

#define MAXNM 32    /* avoid generic defines like SIZE (maxname?) */

typedef struct _node {
    char player[MAXNM];
    struct _node *next;
} node;


(注意:简单地将children设置为node也有帮助,无需创建指向node的全局指针。只需创建一个方便的firstnode即可在代码中使用。)

然后在typedef中,您将同样使用一个名为main()的缓冲区来保存您从用户那里读取的输入,例如

int main (void) {

    char player[MAXNM] = "";
    int nplayers = 0;
    node *list = NULL;


只是附带说明,您不需要多个player语句即可输出多行文本或使文本不会缠绕在页面的一侧。在C中,带引号的字符串是连接在一起的。此外,虽然编译器可能会自动进行更改,但是如果字符串中没有转换说明符,则最好使用printfputs,除非需要,否则避免调用可变参数函数。例如。,

    fputs ( " Welcome To The Count Out Game!\n"
            "------------------------------------\n\n"
            "How Many Children Will Be Playing? : ", stdout );


(为什么在这里使用fputs而不是fputs?-这是一件好事,找出来...)

接下来,您必须验证所有用户输入并处理出现的任何错误。否则,您将迷失在未定义行为的处理垃圾中,并不确定值,直到程序中发生非常糟糕的事情。虽然最好使用puts然后调用fgets解析值(或sscanf等),但如果至少检查返回值,则可以正确使用strtol。这样,您可以验证至少发生了预期的转化次数,并且变量中输入了有效的内容:

    if (scanf ("%d", &nplayers) != 1 || nplayers < 1) {
        fputs ("error: invalid integer input.\n", stderr);
        return 1;
    }


但是,使用scanf的陷阱是它将尾随的scanf(由用户按Enter生成)留在输入缓冲区中,并且在使用'\n'或其他非数字或其他字符进行输入之前,必须先进行处理除fgets之外的转换说明符(它本身还会添加一个其他问题列表,这些问题是由它在第一个空格处停止读取的事实造成的,因此,如果在空格后还有其他/偶然的字符,则您会遇到麻烦)

因此,您可以删除输入缓冲区(此处为"%s")中剩余的所有其他字符。您可以这样做,简单地循环并stdin(如果从另一个打开的文件流中读取,则为getchar()),例如

    /* remove any additional characters from stdin */
    for (int c = getchar(); c != '\n' && c != EOF; c = getchar()) {}


这使我们进入了输入播放器名称的输入循环,您将在其中调用fgetc()(或您的insertnode)开始将节点添加到列表中(并显示createList的完成)

    /* prompt for player and insert node in list */
    while (nplayers-- && 
            fputs ("name: ", stdout) != EOF && 
            fgets (player, MAXNM, stdin)) {
        player[strcspn (player, "\n")] = 0;     /* trim '\n' from end */
        if (!insertnode (&list, player))
            break;
    }

    displaylist (list);     /* output all players in list */
    freelist (list);        /* free list memory */

    return 0;
}


请注意上面的main()调用位置。您正在将列表的地址传递给插入函数。您这样做是为了如果列表地址(即第一个节点)发生更改,则新的列表地址将在调用函数中可用。如果无法传递列表指针的地址,则需要从函数返回列表地址,并在每次返回调用函数时将其分配。

您还需要使用有意义的返回类型声明您的函数,该返回类型可以指示插入操作的成功/失败。简单地将指针返回成功时插入的节点很方便,或者如果失败则insertnode (&list, player)很好。

在插入函数中,除了验证NULL不是player外,您还需要确定列表是否存在,如果不存在,则将新节点添加为第一个节点(将NULL指针设置为指向自身) ,-或-您需要迭代到列表的末尾并在其中插入新节点。每次确保next指针都指向列表地址。一个简单的实现是:

node *insertnode (node **list, char *player)
{
    /* validate player not NULL, handle error */
    if (!player)
        return NULL;

    /* allocate/validate new node */
    node *newnode = malloc (sizeof *newnode);
    if (!newnode) {
        perror ("malloc-newnode");
        return NULL;
    }
    /* copy player to node, initialize next NULL */
    strcpy (newnode->player, player);
    newnode->next = NULL;

    /* check whether list exists, or is this 1st node? */
    if (!*list) {
        newnode->next = newnode;    /* circular list is self-referencial */
        *list = newnode;
    }
    else {  /* list exist, find last node in circular list */
        node *iter = *list;         /* create pointer to iterate to end */
        for (; iter->next != *list; iter = iter->next) {}   /* iterate */
        iter->next = newnode;       /* insert as last node */
        newnode->next = *list;      /* circular, set next to *list */
    }

    return newnode;     /* return node as convenience & success/failure */
}


对于您要为列表编写的其他任何功能,只需拉出铅笔并确定如何遍历列表以从列表中获取所需数据即可。例如,您可以提供打印列表或next函数以及释放与该列表关联的内存的函数。

注意如何处理迭代的精妙之处(在displaylist()函数开始删除第二个节点的情况下,以确保freelist指针引用一个有效地址来指示迭代的结束),例如

void displaylist (node *list)
{
    node *iter = list;

    /* iterate from first to last node, setting iter NULL after last */
    for (; iter; iter = (iter->next != list ? iter->next : NULL))
        puts (iter->player);
}

void freelist (node *list)
{
    node *victim = list->next,  /* free 2nd node 1st, leaving valid 1st */
        *next = NULL;

    while (victim != list) {    /* while victim isn't list address */
        next = victim->next;    /* save next address before free */
        free (victim);          /* free victim */
        victim = next;          /* assign next to victim */
    }

    free (victim);  /* free 1st node */
}


综上所述,您将获得以下内容:

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

#define MAXNM 32    /* avoid generic defines like SIZE (maxname?) */

typedef struct _node {
    char player[MAXNM];
    struct _node *next;
} node;

node *insertnode (node **list, char *player);
void displaylist (node *list);
void freelist (node *list);

int main (void) {

    char player[MAXNM] = "";
    int nplayers = 0;
    node *list = NULL;

    fputs ( " Welcome To The Count Out Game!\n"
            "------------------------------------\n\n"
            "How Many Children Will Be Playing? : ", stdout );

    if (scanf ("%d", &nplayers) != 1 || nplayers < 1) {
        fputs ("error: invalid integer input.\n", stderr);
        return 1;
    }
    /* remove any additional characters from stdin */
    for (int c = getchar(); c != '\n' && c != EOF; c = getchar()) {}

    /* prompt for player and insert node in list */
    while (nplayers-- && 
            fputs ("name: ", stdout) != EOF && 
            fgets (player, MAXNM, stdin)) {
        player[strcspn (player, "\n")] = 0;     /* trim '\n' from end */
        if (!insertnode (&list, player))
            break;
    }

    displaylist (list);     /* output all players in list */
    freelist (list);        /* free list memory */

    return 0;
}

node *insertnode (node **list, char *player)
{
    /* validate player not NULL, handle error */
    if (!player)
        return NULL;

    /* allocate/validate new node */
    node *newnode = malloc (sizeof *newnode);
    if (!newnode) {
        perror ("malloc-newnode");
        return NULL;
    }
    /* copy player to node, initialize next NULL */
    strcpy (newnode->player, player);
    newnode->next = NULL;

    /* check whether list exists, or is this 1st node? */
    if (!*list) {
        newnode->next = newnode;    /* circular list is self-referencial */
        *list = newnode;
    }
    else {  /* list exist, find last node in circular list */
        node *iter = *list;         /* create pointer to iterate to end */
        for (; iter->next != *list; iter = iter->next) {}   /* iterate */
        iter->next = newnode;       /* insert as last node */
        newnode->next = *list;      /* circular, set next to *list */
    }

    return newnode;     /* return node as convenience & success/failure */
}

void displaylist (node *list)
{
    node *iter = list;

    /* iterate from first to last node, setting iter NULL after last */
    for (; iter; iter = (iter->next != list ? iter->next : NULL))
        puts (iter->player);
}

void freelist (node *list)
{
    node *victim = list->next,  /* free 2nd node 1st, leaving valid 1st */
        *next = NULL;

    while (victim != list) {    /* while victim isn't list address */
        next = victim->next;    /* save next address before free */
        free (victim);          /* free victim */
        victim = next;          /* assign next to victim */
    }

    free (victim);  /* free 1st node */
}


使用/输出示例

其用法的一个简短示例是:

$ ./bin/ll-cir_players
Welcome To The Count Out Game!
------------------------------------

How Many Children Will Be Playing? : 5
name: Tom
name: Dick
name: Harry
name: Gus
name: Sarah
Tom
Dick
Harry
Gus
Sarah


内存使用/错误检查

在您编写的任何可以动态分配内存的代码中,对于任何分配的内存块,您都有2个责任:(1)始终保留指向该内存块起始地址的指针,因此,(2)在不分配该内存块时可以将其释放需要更长的时间。

必须使用一个内存错误检查程序来确保您不尝试访问内存或不在分配的块的边界之外/之外写,尝试读取或基于未初始化的值进行条件跳转,最后确认您可以释放已分配的所有内存。

对于Linux,last->next是通常的选择。每个平台都有类似的内存检查器。它们都很容易使用,只需通过它运行程序即可。

$ valgrind ./bin/ll-cir_players
==29803== Memcheck, a memory error detector
==29803== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==29803== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==29803== Command: ./bin/ll-cir_players
==29803==
Welcome To The Count Out Game!
------------------------------------

How Many Children Will Be Playing? : 5
name: Tom
name: Dick
name: Harry
name: Gus
name: Sarah
Tom
Dick
Harry
Gus
Sarah
==29803==
==29803== HEAP SUMMARY:
==29803==     in use at exit: 0 bytes in 0 blocks
==29803==   total heap usage: 5 allocs, 5 frees, 200 bytes allocated
==29803==
==29803== All heap blocks were freed -- no leaks are possible
==29803==
==29803== For counts of detected and suppressed errors, rerun with: -v
==29803== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)


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

现在,此操作的完成时间比预期的要长得多,但是很显然,您在理解和处理单链接循环链接列表方面相当迷失。希望这会给您基本的了解,并让您从这里继续前进。

关于c - 在循环链接列表中存储用户输入的名称,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52886521/

相关文章:

c - 看来 setlocale() 在链接库中不起作用

c - 如何在 C 中初始化字符串数组?

c - 什么是 C 中链表的忠实替代品?

javascript - 仅通过 javascript 从下拉列表中删除重复项

java - 尝试将数字的数字放入数组中,但结果相反

java - Java 链表中的指针

c - 为什么 while 循环对于这个链接列表程序不起作用?

c - GStreamer 将 appsrc 附加到另一个管道

c - 在 C 中处理后获取字符串并返回相同的函数

c - 单指针和双指针追加的区别