释放指针为什么导致奇怪的行为和崩溃?

huangapple go评论68阅读模式
英文:

Why is freeing pointers resulting in weird behavior and crashed?

问题

我在C语言中创建了链表以及一些帮助我操作链表的函数。

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

typedef struct {
    int value;
    void *next;
} Node;

Node *NewNode(int value, Node *next) {
    Node *new = malloc(sizeof(Node));
    new->value = value;
    new->next = next;
    return new;
}


typedef struct {
    Node *head;
} List;

List NewList() {
    List lst = { .head = NULL };
    return lst;
}

void Erase(List *lst) {
    Node *current = lst->head;

    while (current != NULL) {
        Node *next = current->next;
        free(current);
        current = next;
    }

    lst->head = NULL;
}

void PrintList(List *lst) {
    Node *temp = lst->head;

    printf("[ ");

    while (temp != NULL) {
        printf("%d ", temp->value);
        temp = temp->next;
    }

    printf("]\n");
}

void Push(List *lst, int val) {
    Node *new = NewNode(val, lst->head);
    lst->head = new;
}

void Pull(List *lst) {
    if (lst->head == NULL) {
        return;
    }
    Node *firstNode = lst->head;
    lst->head = lst->head->next;
    free(firstNode);
}

int GetAtIndex(List *lst, int index) {
    Node *temp = lst->head;

    for (int i = 0; i < index; i++) {
        temp = temp->next;
    }

    int val = temp->value;

    return val;
}

int main() {
    List lst = NewList();

    Push(&lst, 6);
    Push(&lst, 7);
    Push(&lst, 8);
    Push(&lst, 9);
    PrintList(&lst);  // [ 9 8 7 6 ]

    PrintList(&lst);

    int index = 2;
    printf("lst[%d] = %d\n", index, GetAtIndex(&lst, index));

    PrintList(&lst);

    Erase(&lst);

    return 0;
}

我认为我对指针的工作原理有一个“可以接受的”了解水平。我仍然对何时以及如何释放它们感到困惑。理论上应该很简单...当不再需要它们时释放它们。然而,当我尝试遵循这个原则时,我遇到了一个非常奇怪的问题。

更具体地说,我尝试向GetAtIndex函数中添加一个free(temp)行,如下所示:

int GetAtIndex(List *lst, int index) {
    Node *temp = lst->head;

    for (int i = 0; i < index; i++) {
        temp = temp->next;
    }

    int val = temp->value;
    free(temp);  // 在这里

    return val;
}

这不是一个好的结局。一开始时它起作用,当没有调用其他函数时(如PrintList或另一个GetAtIndex)是可以的。但当我尝试调用其他函数时,程序要么崩溃,要么陷入无限循环。事实上,在PrintList函数中添加free(temp)行时没有出现类似的问题,这更奇怪。

实际上发生了什么?为什么释放正在追踪我的链表成员的指针会导致如此奇怪的行为?或许该问题源于程序的其他部分?最后,为了防止内存泄漏,应该如何释放指针,何时释放指针?

英文:

I created linked lists in C and a few functions to help me with them.

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

typedef struct {
    int value;
    void *next;
} Node;

Node *NewNode(int value, Node *next) {
    Node *new = malloc(sizeof(Node));
    new-&gt;value = value;
    new-&gt;next = next;
    return new;
}


typedef struct {
    Node *head;
} List;

List NewList() {
    List lst = { .head = NULL };
    return lst;
}

void Erase(List *lst) {
    Node *current = lst-&gt;head;

    while (current != NULL) {
        Node *next = current-&gt;next;
        free(current);
        current = next;
    }

    lst-&gt;head = NULL;
}

void PrintList(List *lst) {
    Node *temp = lst-&gt;head;

    printf(&quot;[ &quot;);

    while (temp != NULL) {
        printf(&quot;%d &quot;, temp-&gt;value);
        temp = temp-&gt;next;
    }

    printf(&quot;]\n&quot;);

    free(temp);  // this works wtf?
}

void Push(List *lst, int val) {
    Node *new = NewNode(val, lst-&gt;head);
    lst-&gt;head = new;
}

void Pull(List *lst) {
    if (lst-&gt;head == NULL) {
        return;
    }
    Node *firstNode = lst-&gt;head;
    lst-&gt;head = lst-&gt;head-&gt;next;
    free(firstNode);
}


int GetAtIndex(List *lst, int index) {
    Node *temp = lst-&gt;head;

    for (int i = 0; i &lt; index; i++) {
        temp = temp-&gt;next;
    }

    int val = temp-&gt;value;

    return val;
}


int main() {
    List lst = NewList();

    Push(&amp;lst, 6);
    Push(&amp;lst, 7);
    Push(&amp;lst, 8);
    Push(&amp;lst, 9);
    PrintList(&amp;lst);  // [ 9 8 7 6 ]

    PrintList(&amp;lst);

    int index = 2;
    printf(&quot;lst[%d] = %d\n&quot;, index, GetAtIndex(&amp;lst, index));

    PrintList(&amp;lst);

    Erase(&amp;lst);

    return 0;
}

I think I understand how pointers work to a "decent" level. What I'm still confused with is when to free them and how. In theory it should be simple... Free them when they aren't needed anymore. However I encountered a really strange issue when trying to follow that principle.

More precisely, I tried to add a free(temp) line to the GetAtIndex function, like this:

int GetAtIndex(List *lst, int index) {
    Node *temp = lst-&gt;head;

    for (int i = 0; i &lt; index; i++) {
        temp = temp-&gt;next;
    }

    int val = temp-&gt;value;
    free(temp);  // over here
    return val;
}

That didn't end well. It worked at first, when no other functions were invoked afterwards (like PrintList or another GetAtIndex). But when I did try invoking other functions the program either crashed or ended up in an infinite loop. The fact that a similar issue didn't occur when I added a free(temp) line to the PritList function is even weirder...

What's actually going on here? Why is freeing a pointer that are just tracking my linked list members resulting in such weird behavior? Or maybe that issue stems from other parts of the program? And lastly, how and when should one free pointers to prevent memory leaks?

答案1

得分: 3

`free(temp);` 返回 `temp` 指向的内存。在这种情况下,它是一个节点,这是有问题的,因为它仍然是链表的一部分。在 `PrintList()` 中,你从已经被释放的内存中读取数据,这会导致未定义的行为 (UB)。在 `Erase()` 中,你可能会再次释放一个节点,这被称为 "双重释放",也是 UB

0. 建议使用 `struct Node *` 而不是 `void *` 来表示 `next`。

1. `malloc()` 在失败时返回 NULL。如果你不检查它,当你尝试解引用该指针时,会导致段错误。

2. `Node *NewNode(int value, Node *next)` 堆上分配了一个节点,但 `NewList()` 是在栈上分配的。更合理的设计是保持一致。由于 `List` 只跟踪一个 `head` 指针,我建议使用变量 `Node *lst` 代替类型。这意味着当你想要更改调用者的 `*lst` 值时,你需要传递一个 `Node **lst`。

3. 对于迭代,建议使用 `for` 循环而不是 `while` 循环。它们更适合迭代操作。

4.  C 中,参数按值传递,因此你可以消除临时变量,直接使用参数(`lst`)。参见 `Erase()`、`PrintList()`、`GetAtIndex()`(两次)。

5. `Push()` 没问题。另一种接口是返回新的根节点:

    ```c
    Node *Push(Node *lst, int val) {
      return NewNode(val, lst);
    }
    
    // ...
    
    lst = Push(lst, 6);
    ```
    
    `Push()`  `NewNode()` 之间唯一的区别是参数的顺序。你可以消除其中一个并更新调用者。或者,如果你不想要包装函数,可以在当前实现和上述实现中考虑使用一个临时变量,以防 `malloc()` 失败。

6. 如果 `index` 大于元素数量,`GetAtIndex()` 将导致段错误。当前的接口没有返回错误的方式,所以我修改了它,使用新的输出参数 `value` 返回操作的状态,可以是 `EXIT_SUCCESS`  `EXIT_FAILURE`。考虑使用无符号类型的索引,否则你需要考虑 `GetAtIndex(lst, -17, &value)` 的含义。你想要使用相同类型的索引变量,或者就像我下面的示例中那样将其删除。

    ```c
    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct Node {
    	int value;
    	struct Node *next;
    } Node;
    
    Node *NewNode(int value, Node *next) {
    	Node *new = malloc(sizeof *new);
    	if(!new)
    		return NULL;
    	new->value = value;
    	new->next = next;
    	return new;
    }
    
    void Erase(Node *lst) {
    	while (lst) {
    		Node *next = lst->next;
    		free(lst);
    		lst = next;
    	}
    }
    
    void PrintList(Node *lst) {
    	printf("[ ");
    	for(; lst; lst = lst->next) {
    		printf("%d ", lst->value);
    	}
    	printf("]\n");
    }
    
    void Push(Node **lst, int val) {
    	Node *new = NewNode(val, *lst);
    	*lst = new;
    }
    
    int GetAtIndex(Node *lst, unsigned index, int *value) {
    	for (; index && lst; index--, lst = lst->next);
    	if(!lst)
    		return EXIT_FAILURE;
    	*value = lst->value;
    	return EXIT_SUCCESS;
    }
    
    int main() {
    	Node *lst = NULL;
    	Push(&lst, 6);
    	Push(&lst, 7);
    	Push(&lst, 8);
    	Push(&lst, 9);
    	PrintList(lst);  // [ 9 8 7 6 ]
    	PrintList(lst);
    
    	unsigned index = 2;
        int value;
    	if(GetAtIndex(lst, index, &value) == EXIT_SUCCESS)
    		printf("lst[%u] = %d\n", index, value);
    	else
    		printf("out of bounds\n");
    
    	PrintList(lst);
    	Erase(lst);
    }
    ```
英文:

free(temp); returns the memory that temp points to. In this case it's a node which is problematic as it's still part of the linked list. In PrintList() you read from memory that was fee'ed you have undefined behavior (UB). In Erase() you likely free one of the nodes again which is called a "double free" which is also UB.

  1. Prefer struct Node * instead of void * for next.

  2. malloc() returns NULL on failure. If you don't check it will segfault when you try to deference that pointer.

  3. Node *NewNode(int value, Node *next) heap allocates a node but NewList() is stack allocated. It would be a more sensible design to be consist. As List just tracks a single head pointer I would eliminate the type in favor for a variable Node *lst. It does mean that when you want to change the caller's value of *lst then you need to pass in a Node **lst.

  4. Prefer for-loops to while-loops for iteration. They are built for it.

  5. Arguments are passed by value in C, so your eliminate the temporary variables and just use the argument (lst). See Erase(), PrintList(), GetAtIndex() (twice).

  6. Push() is fine. An alternative interface is to return the new root node:

    Node *Push(Node *lst, int val) {
    return NewNode(val, lst);
    }
    // ...
    lst = Push(lst, 6);
    

    The only difference between Push() and NewNode() is the order of arguments. I would eliminate one of them and update callers. Alternatively you could #define one in terms of other if you don't want the wrapper function.

    In the current implementation and the above consider using a temporary so you don't leak the tail of your list if malloc() fails.

  7. GetAtIndex() will segfault if index is greater than number of elements. The current interface doesn't have a way to return an error so I changed it return the status of operation either EXIT_SUCCESS or EXIT_FAILURE and value in a new out param value. Consider using an unsigned type for indices otherwise you have to consider what happens what GetAtIndex(lst, -17, &amp;value) means. You want to use the same type for the index variable or just eliminate it as I did below.

    #include &lt;stdio.h&gt;
    #include &lt;stdlib.h&gt;
    typedef struct Node {
    int value;
    struct Node *next;
    } Node;
    Node *NewNode(int value, Node *next) {
    Node *new = malloc(sizeof *new);
    if(!new)
    return NULL;
    new-&gt;value = value;
    new-&gt;next = next;
    return new;
    }
    void Erase(Node *lst) {
    while (lst) {
    Node *next = lst-&gt;next;
    free(lst);
    lst = next;
    }
    }
    void PrintList(Node *lst) {
    printf(&quot;[ &quot;);
    for(; lst; lst = lst-&gt;next) {
    printf(&quot;%d &quot;, lst-&gt;value);
    }
    printf(&quot;]\n&quot;);
    }
    void Push(Node **lst, int val) {
    Node *new = NewNode(val, *lst);
    *lst = new;
    }
    int GetAtIndex(Node *lst, unsigned index, int *value) {
    for (; index &amp;&amp; lst; index--, lst = lst-&gt;next);
    if(!lst)
    return EXIT_FAILURE;
    *value = lst-&gt;value;
    return EXIT_SUCCESS;
    }
    int main() {
    Node *lst = NULL;
    Push(&amp;lst, 6);
    Push(&amp;lst, 7);
    Push(&amp;lst, 8);
    Push(&amp;lst, 9);
    PrintList(lst);  // [ 9 8 7 6 ]
    PrintList(lst);
    unsigned index = 2;
    int value;
    if(GetAtIndex(lst, index, &amp;value) == EXIT_SUCCESS)
    printf(&quot;lst[%u] = %d\n&quot;, index, value);
    else
    printf(&quot;out of bounds\n&quot;);
    PrintList(lst);
    Erase(lst);
    }
    

huangapple
  • 本文由 发表于 2023年6月2日 05:50:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/76385943.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定