【问题标题】:Malloc function in dynamic lists动态列表中的 malloc 函数
【发布时间】:2018-06-13 15:58:01
【问题描述】:

我开始使用动态列表,我不明白为什么即使在 main() 程序中声明第一个节点时也需要使用 malloc 函数,下面的代码应该只打印包含的数据在第一个节点中,但如果我不使用 malloc 函数初始化节点,它就不起作用:

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

void insert(int val, struct node*);

int main() {
    struct node* head ;
    head->data = 2;
    printf("%d \n", head->data);

}

【问题讨论】:

  • struct node* head; 只是一个指向 struct node 的指针,它还不存在。您使用malloc 分配内存,在本例中为struct node。您的 head-> 调用未定义的行为,因为 head 尚未指向有效内存。你说它应该只打印第一个节点中的数据..你有零个节点。您必须先为节点分配一些内存才能创建节点。

标签: c list dynamic


【解决方案1】:

从技术上讲,您不会,但是保持所有节点具有相同的内存模式对您来说只是一个优势,没有真正的劣势。

假设所有个节点都存储在动态内存中。

您的“插入”过程最好命名为“add”或(对于完整功能上下文)“cons”,它应该返回新节点:

struct node* cons(int val, struct node* next)
{
  struct node* this = (struct node*)malloc( sizeof struct node );
  if (!this) return next;  // or some other error condition!
  this->data = val;
  this->next = next;
  return this;
}

现在构建列表非常容易:

int main()
{
  struct node* xs = cons( 2, cons( 3, cons( 5, cons( 7, NULL ) ) ) );
  // You now have a list of the first four prime numbers.

而且处理起来很容易。

  // Let’s print them!
  {
    struct node* p = xs;
    while (p)
    {
      printf( "%d ", p->data );
      p = p->next;
    }
    printf( "\n" );
  }

  // Let’s get the length!
  int length = 0;
  {
    struct node* p = xs;
    while (p)
    {
      length += 1;
      p = p->next;
    }
  }
  printf( "xs is %d elements long.\n", length );

顺便说一句,在命名事物时应该尽量保持一致。您已将节点数据命名为“data”,但构造函数的参数将其命名为“val”。你应该选择一个并坚持下去。

此外,以下情况也很常见:

typedef struct node node;

现在除了struct node 的定义之外的所有地方都可以使用node 这个词。

哦,我差点忘了:别忘了用适当的析构函数进行清理。

node* destroy( node* root )
{
  if (!root) return NULL;
  destroy( root->next );
  free( root );
  return NULL;
}

以及main() 的附录:

int main()
{
  node* xs = ...

  ...

  xs = destroy( xs );
}

【讨论】:

  • 最好让destroy() 只返回一个空指针。
  • 不,我不同意。最好返回一个值以分配给调用者的头指针,如示例中所示。但在这种情况下,无论哪种方式,“更好”都是意见。
  • 好的。我明白你为什么要这么做。如果我想确保调用代码中的指针为空,我会使用指向指针参数的指针。但这不值得为之争吵。你有什么是好的。
  • 我已经做到了两种方式。我开始认为这种方式更干净(至少,这是我目前的看法,哈哈),但无论哪种方式都一样好。
【解决方案2】:

当你声明一个变量时,你定义了变量的类型,那么它就是 name 并且可以选择声明它的初始值。

每种类型都需要特定数量的内存。例如int 将是 在 32 位操作系统上为 32 位长,在 64 位操作系统上为 8 位。

在函数中声明的变量通常存储在相关的堆栈中 与功能。当函数返回时,该函数的堆栈是 不再可用,并且该变量不再存在。

当您需要变量的值/对象即使在函数之后也存在时 返回,那么您需要在程序的不同部分分配内存, 通常是堆。这正是mallocrealloccalloc 所做的。

在做

struct node* head ;
head->data = 2;

是错误的。您已经声明了一个名为 head 的指针,类型为 struct node, 但你没有为它分配任何东西。所以它指向一个未指定的 内存中的位置。 head->data = 2 尝试将值存储在未指定的位置 位置和程序很可能会因段错误而崩溃。

你可以这样做:

int main(void)
{
    struct node head;
    head.data = 2;
    printf("%d \n", head.data);
    return 0;
}

head 将被保存在堆栈中,只要main 不存在,它就会一直存在 返回。但这只是一个很小的例子。在一个复杂的程序中,你 有更多的变量、对象等。简单地声明 all 是个坏主意 main 中需要的变量。所以最好是在创建对象时 需要。

例如,您可以有一个创建对象的函数和另一个 调用create_node 并使用该对象。

struct node *create_node(int data)
{
    struct node *head = malloc(sizeof *head);
    if(head == NULL)
        return NULL; // no more memory left

    head->data = data;
    head->next = NULL;

    return head;
}

struct node *foo(void)
{
    struct node *head = create_node(112);

    // do somethig with head

    return head;
}

这里create_node 使用malloc 为一个struct node 分配内存 对象,用一些值初始化对象并返回指向该内存位置的指针。 foo 调用 create_node 并对其进行处理并返回 目的。如果另一个函数调用foo,这个函数将获取对象。

malloc 还有其他原因。考虑这段代码:

void foo(void)
{
    int numbers[4] = { 1, 3, 5, 7 };

    ...
}

在这种情况下,您知道您将需要 4 个整数。但有时你需要一个 仅在运行时才知道元素数量的数组,例如 因为它取决于一些用户输入。为此,您也可以使用malloc

void foo(int size)
{
    int *numbers = malloc(size * sizeof *numbers);

    // now you have "size" elements
    ...
    free(numbers);   // freeing memory
}

当您使用mallocrealloccalloc 时,您需要释放内存。如果 您的程序不再需要内存,您必须使用free(如 最后一个例子。请注意,为简单起见,我省略了 freestruct head 的示例。

【讨论】:

    【解决方案3】:

    您所拥有的会调用未定义的行为,因为您实际上并没有节点,您有一个指向实际上并不指向节点的节点的 指针。使用malloc 和朋友创建一个实际节点对象可以驻留以及节点指针可以指向的内存区域。

    在您的代码中,struct node* head 是一个指向无处的指针,并且像您所做的那样取消引用它是未定义的行为(这通常会导致段错误)。您必须先将 head 指向有效的 struct node,然后才能安全地取消引用它。一种方法是这样的:

    int main() {
        struct node* head;
        struct node myNode;
        head = &myNode;  // assigning the address of myNode to head, now head points somewhere
        head->data = 2;  // this is legal
        printf("%d \n", head->data);  // will print 2
    
    }
    

    但是在上面的例子中,myNode 是一个局部变量,一旦函数存在就会超出范围(在这种情况下是main)。正如您在问题中所说,对于链接列表,您通常希望 malloc 数据,以便可以在当前范围之外使用它。

    int main() {
        struct node* head = malloc(sizeof struct node);
        if (head != NULL)
        {
            // we received a valid memory block, so we can safely dereference
            // you should ALWAYS initialize/assign memory when you allocate it.
            // malloc does not do this, but calloc does (initializes it to 0) if you want to use that
            // you can use malloc and memset together.. in this case there's just
            // two fields, so we can initialize via assignment.
            head->data = 2;
            head->next = NULL;
            printf("%d \n", head->data);
    
            // clean up memory when we're done using it
            free(head);
        }
        else
        {
            // we were unable to obtain memory
            fprintf(stderr, "Unable to allocate memory!\n");
        }
        return 0;
    }
    

    这是一个非常简单的例子。通常对于链表,您将拥有插入函数(通常发生mallocing 的地方和删除函数(通常发生freeing 的地方。你至少会有一个@ 987654332@ 指针始终指向列表中的第一项,对于双链表,您还需要一个 tail 指针。还可以有打印函数、deleteEntireList 函数等。但有一个方法或者,您必须为实际对象分配空间。malloc 是一种这样做的方法,因此内存的有效性在程序的整个运行时都保持不变。


    编辑:

    不正确。这绝对适用于intint*,它适用于任何对象和指向它的指针。如果您有以下情况:

    int main() {
        int* head;
        *head = 2;  // head uninitialized and unassigned, this is UB
        printf("%d\n", *head); // UB again
    
        return 0;
    }
    

    这是您在 OP 中的所有未定义行为。指针必须指向有效的东西,然后才能取消引用它。在上面的代码中,head 未初始化,它没有确定性地指向任何内容,并且一旦您执行*head(无论是读取还是写入),您就会调用未定义的行为。就像您的 struct node 一样,您必须执行以下操作才能正确:

    int main() {
            int myInt;  // creates space for an actual int in automatic storage (most likely the stack)
            int* head = &myInt;  // now head points to a valid memory location, namely myInt
            *head = 2;  // now myInt == 2
            printf("%d\n", *head);  // prints 2
    
            return 0;
        }
    

    或者你可以这样做

    int main() {
            int* head = malloc(sizeof int);  // silly to malloc a single int, but this is for illustration purposes
            if (head != NULL)
            {
                // space for an int was returned to us from the heap
                *head = 2; // now the unnamed int that head points to is 2
                printf("%d\n", *head);  // prints out 2
                // don't forget to clean up
                free(head);
            }
            else
            {
                // handle error, print error message, etc
            }
    
            return 0;
        }
    

    这些规则适用于您正在处理的任何原始类型或数据结构。指针必须指向某些东西,否则取消引用它们是未定义的行为,并且您希望在发生这种情况时得到一个段错误,以便您可以在 TA 评分之前或在客户演示之前追踪错误。墨菲定律规定 UB 总是会在呈现代码时崩溃。

    【讨论】:

    • 好的,非常感谢您的回答,但我仍然不明白为什么在涉及像 int 这样的简单变量时我们不需要遵循相同的过程。例如,当我声明一个指向整数值的指针时,为什么我不需要先声明一个整数,然后将其地址分配给指针,然后才能通过 * 运算符前面的指针实际修改变量的值?跨度>
    • @AntonioPavan 我想你现在已经明白了,但如果你愿意,请查看我的编辑以获得进一步的解释。
    【解决方案4】:

    语句struct node* head; 定义了一个指向节点对象的指针,而不是节点对象本身。由于您没有初始化指针(即通过让它指向由例如 malloc-statement 创建的节点对象),因此像使用 head->data 一样取消引用此指针会产生未定义的行为。

    有两种方法可以克服这个问题,(1) 动态分配内存 - 生成具有动态存储持续时间的对象,或 (2) 将对象本身定义为,例如,具有自动存储持续时间的局部变量:

    (1) 动态存储时长

    int main() {
        struct node* head = calloc(1, sizeof(struct node));
        if (head) {
          head->data = 2;
          printf("%d \n", head->data);
          free(head);
        }
    }
    

    (2) 自动存储时长

    int main() {
        struct node head;
        head.data = 2;
        printf("%d \n", head.data);
    }
    

    【讨论】:

    • 好的,非常感谢您的回答,但我仍然不明白为什么我们在涉及像 int 这样的简单变量时不需要遵循相同的过程。例如,当我声明一个指向整数值的指针时,为什么我不需要先声明一个整数,然后将其地址分配给指针,然后才能通过 * 运算符前面的指针实际修改变量的值?跨度>
    • 其实没有区别; int *iptr; *iptr=10; 也会产生未定义的行为。
    • 再次感谢,不知怎么的,我的想法正好相反。
    猜你喜欢
    • 1970-01-01
    • 2012-03-19
    • 2012-04-29
    • 2012-06-24
    • 1970-01-01
    • 2017-01-18
    • 2021-03-29
    • 1970-01-01
    • 2020-08-04
    相关资源
    最近更新 更多