Search code examples
cpointersmemory-managementlinked-listfree

Why is freeing pointers resulting in weird behavior and crashed?


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

#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");

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

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;
}

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->head;

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

    int val = temp->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?


Solution

  • 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, &value) means. You want to use the same type for the index variable or just eliminate it as I did below.

      #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);
      }