在C语言中实现链表时,为什么要使用动态内存分配(如malloc())?

3

好的,这个问题对于菜鸟程序员可能听起来很愚蠢。但是,这个问题真的让我困扰,我希望能得到一个充分的解答。我刚刚开始学习数据结构课程,以下是让我困扰的问题:

假设使用C语言,

//Implementing a node

struct Node
{
     int data;
     struct *Node;
};

现在,在创建节点时,为什么要使用动态内存分配技术,而使用malloc()。我们不能只创建一个类型为“Struct Node”的变量吗?
即类似于:
struct Node N1;
 //First node - actually second where !st Node is assumed to be Head.

struct Node *Head = &N1;
struct Node N2;
N2.(*Node) = &N1;

我的代码可能有些错误,因为我只是一个初学者,对C语言不是很熟悉。但是现在你可能已经理解了我想表达的基本意思。为什么不创建Node类型的变量,并将它们存储在Node类型的数组中以分配内存给新节点,而要涉及到动态内存分配的复杂性呢?


5
当然,您可以只使用已声明的变量来创建链表。但是这样会受制于已声明变量的数量,导致链表的最大大小受限。 - Cheers and hth. - Alf
4
可以,但是(1)您需要为每个下一个节点创建一个新的硬编码变量,(2)这些本地变量在离开其封闭代码块后不会保留。 - Jongware
4
如果你想要一个“动态”的容器,就需要一个动态的存储源。 - Kerrek SB
6个回答

6

首先,你在声明结构体时有错误。 struct * 本身并不表示一种类型。你必须给出完整的类型名称:

struct Node
{
     int data;
     struct Node *Node;
};

您可以像上面那样使用局部变量来创建链表,但这会限制您的链表元素数量,即只有您显式声明的元素。这也意味着您无法在函数中创建链表,因为那些变量会超出其作用域。

例如,如果您这样做:

struct Node *getList()
{
    struct Node head, node1, node2, node3;
    head.Node = &node1;
    node1.Node = &node2;
    node2.Node = &node3;
    node3.Node = NULL;
    return &head;
}

如果您只能使用固定大小的数组,那么列表的长度将被限制在4个元素以内。如果您需要成千上万个元素怎么办?而且,通过返回局部变量的地址,当函数返回时它们超出了范围,访问它们会导致未定义行为

通过动态分配每个节点,您只受可用内存的限制。

以下是使用动态内存分配的示例:

struct Node *getList()
{
    struct Node *head, *current;
    head = NULL;
    current = NULL;

    // open file
    while (/* file has data */) {
        int data = /* read data from file */
        if (head == NULL) {      // list is empty, so create head node
            head = malloc(sizeof(struct Node *));
            current = head;
        } else {                 // create new element at end of list
            current->next = malloc(sizeof(struct Node *));
            current = current->next;
        }
        current->data = data;
        current->Node = NULL;
    }
    // close file
    return head;
}

这是伪代码,没有详细说明如何读取相关数据,但您可以看到如何创建一个存在于程序生命周期中的任意大小的列表。

2
如果这些变量是“局部的”,即在函数范围内定义(即存储在堆栈上),那么你不应该这样做,因为在离开其范围后访问它们将导致未定义的行为(随着调用其他函数,它们的内容可能会被覆盖)。实际上,每当你从函数返回指向局部变量的指针时,你都在做错事。由于C语言的性质,这是有问题的,因为没有任何东西会警告你正在做错事,只有在尝试再次访问此区域时才会失败。
另一方面,如果它们声明为全局变量(在任何其他函数之外),则你只受限于以这种方式声明的变量数量。
你可以潜在地声明许多变量,但跟踪哪个变量“空闲”可用将是痛苦的。当然,你甚至可以更进一步,说你将拥有一个全局预分配的节点数组,以防止使用malloc,但是在你做所有这些的过程中,你只是越来越接近编写自己的版本malloc,而不是坚持现有的动态版本。
此外,如果你不使用所有预分配的空间,则所有预分配的空间都将浪费,而且你无法在运行时动态地增长列表(因此称为动态分配)。

0

这里有一些使用动态内存的好理由

  1. 当你声明节点 struct Node N1; 时,该节点将存储在堆栈内存中。在节点范围之后,它将自动销毁。但是,在动态内存中,你需要在完成后释放内存。

  2. 当你有一些内存限制时。

  3. 当你不知道数组大小时,动态内存分配将会帮助你。


0

一个问题可能是您无法使用另一个函数向列表添加新节点。

请记住,自动变量(例如由struct Node node100;创建的变量)仅在定义它们的函数内部具有作用域。因此,当您执行以下操作时:

int main()
{
    struct Node *head;
    /* Some code there you build list as:
       head ---> node1 ---> node2 --> .. ---> node99 
    */

    /* Add a new node using add_node function */
    add_node(head, 555);

    /* Access the last node*/
}

void add_node(struct Node *head, int val)
{
     /* Create new node WITHOUT using malloc */
     struct Node new_node;
     new_node.data = val;

     /* add this node to end of the list */
     /* code to add this node to the end of list */
     /* last_element_of_list.next = &new_node*/

     return;
}

现在你认为已经将一个新节点添加到了列表的末尾。但是,不幸的是,它的生命周期在add_node函数返回时就结束了。当你尝试在main函数中访问最后一个节点时,程序会崩溃。

因此,为了避免这种情况,你需要将所有代码放在一个单一的函数中,以便这些节点的生命周期不会结束。

将所有代码放在一个函数中是不好的实践,会导致许多困难。

这是需要动态内存分配的一种情况,因为使用malloc分配的节点将在使用free释放之前一直存在,并且你可以将执行不同任务的代码放在不同的函数中,这是一个好的实践。


0

创建链表时不一定需要使用动态内存,但绝对不要为每个节点创建单独的变量。如果您想要存储多达N个项目,则需要声明N个不同的变量,当N变大时,这将变得非常麻烦。使用链表的整个思想是它可以根据需要增长或缩小;它是一种动态数据结构,因此即使您不使用mallocfree,您最终会做类似的事情。

例如,您可以在文件作用域中创建一个节点数组,如下所示:

struct node {
  int data;
  struct node *next;
};

/**
 *  use the static keyword to keep the names from being visible 
 *  to other translation units
 */
static struct node store[N];  /* our "heap" */
static struct node *avail;    /* will point to first available node in store */

你需要初始化数组,使每个元素都指向下一个元素,最后一个元素则指向NULL

void initAvail( void )
{
  for ( size_t i = 0; i < N - 1; i++ )
    store[i].next = &store[i + 1];
  store[N - 1].next = NULL;
  avail = store;
}

为了为您的列表分配节点,我们获取节点avail指向并更新avail以指向下一个可用节点(如果availNULL,则没有更多可用节点)。
struct node *getNewNode( void )
{
  struct node *newNode = NULL;

  if ( avail ) /* if the available list isn't empty */
  {
    newNode = avail;       /* grab first available node */
    avail = avail->next;   /* set avail to point to next available node */
    newNode->next = NULL;  /* sever newNode from available list, */
  }                        /* which we do *after* we update avail */
                           /* work it out on paper to understand why */
  return newNode;
}

当你完成一个节点时,将其添加回可用列表的头部。
void freeNode( struct node *n )
{
  n->next = avail;
  avail = n;
}

我们不是在使用动态内存,也就是说我们没有调用“mallic”或“free”; 但是我们已经基本重现了动态内存的功能,只是额外的限制是我们的“堆”有一个固定的上限大小。
请注意,一些嵌入式系统并没有像这样的堆,因此您必须采取类似于这样的方法来在这些系统上实现列表。

0
你可以不使用malloc来编写单向链表,但是确保实现在主函数中完成。但是如果要编写遍历、查找最小数等程序,这些结构体变量将会超出作用域。
struct node{
    int a;
    struct node* nextNode;
};

int main()
{
    struct node head,node1,node2;
    head.a=45;
    node1.a=98;
    node2.a=3;
    head.nextNode=&node1;
    node1.nextNode=&node2;
    node2.nextNode=NULL;

    if(head.nextNode== NULL)
    {
        printf("List is empty");
    }
    struct node* ptr=&head;
    while(ptr!=NULL)
    {
        printf("%d ",ptr->a);
        ptr=ptr->nextNode;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接