Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 46

Modifying Linked List Head with Double Pointers

Recommended resources

Introduction

In our last lesson, we built functions to search for and delete nodes from our game inventory. We saw that deleting the head node was a special case requiring careful pointer manipulation. This was because any function that needs to change which node is the first in the list must be able to modify the head pointer itself, not just the data it points to.

This lesson drills down into the core C mechanism that makes this possible: the double pointer. We'll move beyond simply using it and explore why it's necessary, cementing a crucial concept for low-level programming. You will learn how C's pass-by-value nature applies to pointers and how using a pointer-to-a-pointer (struct Node **) allows us to effectively "pass a pointer by reference," enabling functions to directly modify pointer variables from a calling function's scope. Mastering this is a significant step toward writing the kind of robust, memory-aware C code required for game engines and other high-performance systems.


The Problem: C is Always Pass-by-Value

A common point of confusion in C is how function arguments are passed. The rule is simple and absolute: C always passes arguments by value. This means the function receives a copy of the argument, not the original variable.

When you pass a standard variable like an int, this is intuitive. If you pass a pointer, the same rule applies: the function receives a copy of the pointer. The pointer is an address, so the function gets a copy of that memory address.

This has a critical implication:

  • You can use the copied pointer to go to that address and change the data stored there (e.g., ptr->data = 100;).
  • You cannot change where the original pointer in the calling function is pointing. If you reassign the pointer inside the function (e.g., ptr = newNode;), you are only changing the local copy. The original pointer remains unaffected.

This is why a function like void brokenInsert(struct Node* head, int value) cannot change the list's head if the list is initially empty. The head in main is NULL. A copy of this NULL is passed to the function. Even if the function allocates a new node and sets its local head variable to point to it, the original head in main is still NULL.

The following Stack Overflow discussion provides an excellent explanation of this exact problem. Pay close attention to the answer that talks about "copy by value."

What is the reason for using a double pointer when adding a node in ...

This Stack Overflow thread addresses the core question of this lesson.

First, read the answer that begins "copy by value". This frames the problem perfectly. Then, read the short, concise answer that lists the two workarounds. This sets the stage for our solution.


The Solution: Pointers to Pointers

If we want a function to modify a variable, we must pass the address of that variable. So, if we want a function to modify a pointer, we must pass the address of the pointer. A variable that stores the address of a pointer is called a pointer to a pointer, or a double pointer.

  • An int* stores the address of an int.
  • An int** stores the address of an int*.
  • A struct Node* stores the address of a struct Node.
  • A struct Node** stores the address of a struct Node*.

Let's watch a video that breaks down the mechanics and syntax of declaring, assigning, and dereferencing double pointers.

Pointers to Pointers in C/C++

This video from mycodeschool provides a clear, step-by-step introduction to the concept of multi-level pointers.

Watch the following segments to build a solid foundation: Pointer to pointer: Understand the declaration int **q and the idea of q storing the address of p. Chains of indirection: See how this concept can be extended to int ***r and beyond. Dereferencing: This is the most crucial part. Observe how *q gives you the value of p (an address), and **q gives you the value at that address (an integer). Modification: See how you can use **q or ***r to modify the original variable x.

The primary use case for this in practical C programming, and the one we care about, is to simulate pass-by-reference for pointers.

Making Peace with Double Pointers

This video from Jacob Sorber, "Making Peace with Double Pointers," gets straight to the point of why we use them.

Watch the section from the primary use case. The speaker explains exactly why C's pass-by-value nature necessitates using a double pointer when a function needs to change the caller's pointer.


Applying Double Pointers to Linked Lists

Now let's apply this to our linked list insertAtFront function. We have two ways to design this function:

  1. Return the new head:

    struct Node* insertAtFront(struct Node* head, int value);
    // Caller must do: head = insertAtFront(head, 10);
    

    This works, but it's risky. The caller must remember to assign the return value back to their head pointer. Forgetting to do so is a common bug that leads to a silent failure (the node is created but the list isn't updated) and a memory leak.

  2. Use a double pointer:

    void insertAtFront(struct Node** head_ref, int value);
    // Caller simply does: insertAtFront(&head, 10);
    

    This approach is often considered more robust. The function directly modifies the caller's head pointer. There is no return value to forget. The function signature void makes it clear that the function accomplishes its goal via a side effect—in this case, modifying the data at head_ref.

The image below provides a fantastic visualization of how the double pointer approach works, showing the state of the stack and heap at each step.

A step-by-step visual trace of the `insertAtFront` function using a double pointer. `headPtr` on the stack holds the address of `head` in `main`, allowing the function to modify the original `head` pointer.

Let's break down what's happening in the code shown in the image:

  • int main(void): A pointer head is created on the main function's stack frame. Initially, it points to the node with data 1.
  • insertAtFront(&head, 0): We call the function, passing the address of the head pointer.
  • bool insertAtFront(Node **headPtr, ...):
    1. The parameter headPtr (a local variable on insertAtFront's stack frame) now holds the memory address of the head pointer from main.
    2. Node *temp = createNode(value);: A new node (with data 0) is allocated on the heap. The local pointer temp points to it.
    3. temp->next = *headPtr;: This is the first key step. We dereference headPtr once. *headPtr gives us the value of the original head pointer from main (the address of the node with data 1). We set our new node's next field to point to the old head.
    4. *headPtr = temp;: This is the second and most crucial step. We are again dereferencing headPtr to access the original head pointer from main. We then assign a new value to it: the address of our newly created node, which is stored in temp. This directly changes the head pointer in main to point to the new node. The insertion is complete.

The Height of Elegance: The Linus Torvalds Method

Mastering double pointers opens the door to more advanced and elegant list-manipulation algorithms. In our previous lesson, we treated deleting the head node as a special case. With a clever use of double pointers, you can handle head deletion and middle-of-the-list deletion with the exact same code, eliminating the if/else branching.

This technique, famously advocated by Linus Torvalds, involves using a double pointer that tracks the pointer to the current node. Initially, it points to the head pointer itself. As you traverse, it points to the next field of the previous node. This way, no matter which node you delete, you are always just modifying the pointer that led to it.

What is the reason for using a double pointer when adding a node in ...

Let's revisit the Stack Overflow thread to see what a master C programmer has to say.

Read the answer that contains a quote from Linus Torvalds. Focus on his explanation of how to delete a node without any conditional logic for the head.

While we won't implement this "pointer-to-the-entry-pointer" method in our main project just yet, understanding the concept is a hallmark of truly getting how pointers work. The code below from another resource shows what this removeNode function looks like.

An Elegant and Efficient Linked List Implementation

This article presents a full implementation of the elegant double-pointer technique.

Look at the code in Example 2. Specifically, study the removeNode function. Notice how the pnp (pointer-to-next-pointer) is updated inside the loop, and how the line *pnp = node->next; works correctly whether pnp holds the address of the list's head or the address of a next field within a node.

This is the kind of concise, powerful code that a deep understanding of pointers enables you to write.


Conclusion

This lesson tackled one of the more challenging but powerful concepts in C. By understanding that C is always pass-by-value and that pointers are themselves variables with addresses, you can now see why double pointers are the correct tool for modifying a caller's pointer variable.

Key Takeaways:

  • Functions receive copies of their arguments. You can't change a caller's variable by reassigning the copy within the function.
  • To modify a caller's pointer (e.g., the head of a linked list), you must pass its address.
  • A variable holding the address of a pointer is a double pointer (e.g., struct Node**).
  • Within a function that takes a double pointer head_ref, you use *head_ref to access and modify the original head pointer from the calling function.
  • This technique allows for more robust function design, as it prevents bugs where a caller forgets to assign the return value of a function like insertAtFront.

In our next lesson, we'll complete our inventory management toolkit by writing a function to destroy an entire list. This will involve traversing every node and carefully deallocating its memory with free(), giving you more practice with traversal and solidifying the principles of manual memory management.