Hello! Welcome to the next lesson in our journey through C pointers.
In the previous lesson, we learned how to write robust functions that handle memory allocation failures, often by returning NULL to the caller. This raised a crucial question: what if a function's job is to allocate memory and assign it to a pointer provided by the caller? Since C passes arguments by value, simply passing a pointer won't work, as the function would only modify its local copy.
Today, we solve that problem by introducing a fundamental concept: the pointer to a pointer, or double pointer. This mechanism allows a function to reach back into its caller's stack frame and modify the original pointer variable. Mastering this concept is key to building flexible APIs and understanding many advanced data structures in C.
By the end of this lesson, you will be able to declare and use pointers to pointers (double pointers) effectively.
What is a Pointer to a Pointer?
A regular pointer is a variable that stores the memory address of another variable (like an int or a struct). A pointer to a pointer is simply a variable that stores the memory address of another pointer. It adds another layer of indirection.
Think of it this way:
- A variable holds a value (e.g.,
42). - A pointer holds the address of that value (e.g., address
0x7fff...). - A double pointer holds the address of the pointer (e.g., address
0x7ffe..., which is where the address0x7fff...is stored).
The following diagram illustrates this chain of references.

To get a clear visual and code-based introduction, let's watch a short video.
The video 'Double Pointers in C' by Logic Lambda provides an excellent step-by-step breakdown of the memory layout and syntax for double pointers.
Please watch from the beginning to 05:24. The video will cover: The definition of a double pointer. A visualization of how an integer, a single pointer, and a double pointer are laid out in memory. A code walkthrough demonstrating the declaration, initialization, and dereferencing of each, showing the addresses and values at every step.
Declaration, Initialization, and Dereferencing
As you saw in the video, the syntax for declaring a double pointer involves two asterisks (**).
Let's break down the mechanics with a code snippet:
#include <stdio.h>
int main() {
int value = 99;
// A regular pointer to an integer
int *ptr = &value;
// A pointer to a pointer-to-an-integer
int **dptr = &ptr;
printf("--- Addresses ---\n");
printf("Address of 'value': %p\n", &value);
printf("Address of 'ptr': %p\n", &ptr);
printf("Address of 'dptr': %p\n", &dptr);
printf("\n--- Values Stored in Pointers ---\n");
printf("Value stored in 'ptr' (address of 'value'): %p\n", ptr);
printf("Value stored in 'dptr' (address of 'ptr'): %p\n", dptr);
printf("\n--- Dereferencing ---\n");
printf("Value of 'value': %d\n", value);
printf("Value via single dereference (*ptr): %d\n", *ptr);
printf("Value via double dereference (**dptr): %d\n", **dptr);
printf("\n--- Intermediate Dereference ---\n");
// *dptr gives you the value of ptr, which is the address of 'value'
printf("Value of *dptr (same as ptr): %p\n", *dptr);
return 0;
}
Here's the key to understanding dereferencing:
dptr: The double pointer variable itself. Its value is the memory address ofptr.*dptr: The first dereference. It "follows the pointer" once to give you the contents ofptr. The content ofptris the address ofvalue. So,*dptris equivalent toptr.**dptr: The second dereference. This is equivalent to*(*dptr), which is equivalent to*(ptr). This follows the pointer chain all the way to the end to retrieve the integer99.
If you prefer a textual explanation, the following resource provides a solid summary.
Understanding Pointer to Pointer in C(Double Pointer)
The article 'Understanding Pointer to Pointer in C' from CCBP reinforces these concepts with clear syntax examples and explanations.
Read the sections 'Declaration and Initialization of Pointer to Pointer', 'How Double Pointer Works?', and 'Referencing and Dereferencing'. These sections provide a good textual summary of what was covered in the video.
Use Case: Modifying a Caller's Pointer
Now, let's address the problem from our introduction. How can a function modify a pointer that belongs to its caller? You do this by passing the address of the pointer.
Let's look at a common scenario: a function that allocates memory and returns it through a parameter. This is a frequent pattern in C APIs where the function's return value is used to signal success or failure.
How do pointer-to-pointers work in C?
This Stack Overflow answer provides a concise and practical example of this exact use case.
Read the answer by 'Brian R. Bondy'. Focus on the example function get1024HeapMemory(int **p) and how it's called with get1024HeapMemory(&p). This perfectly demonstrates how to use a double pointer as an 'out parameter' to modify the caller's pointer.
Let's analyze the pattern from the resource:
- The Caller (
main): Has a pointer,int *p, which is uninitialized. It wants theget1024HeapMemoryfunction to makeppoint to a valid block of allocated memory. - Passing the Argument: To allow the function to change
p,mainpasses its address:&p. The type of&pisint **. - The Function (
get1024HeapMemory): Declares its parameter asint **p_addr. Inside the function,p_addrholds the address of the caller'sp. - Modification: To access the caller's original
p, the function dereferences its parameter:*p_addr. It then assigns the result ofmallocto*p_addr. This action directly modifies thepvariable back inmain.
This technique is essential for your toolkit and is used extensively in C libraries. The video "Making Peace with Double Pointers" also explains this concept clearly, using an example modeled on the POSIX pthread_join function.
Making Peace with Double Pointers
Let's watch a segment that reinforces this core concept from a different perspective.
Watch from 01:21 to 02:59. The key insight here is the explanation of why a single pointer fails due to pass-by-value semantics, and how the double pointer solves it.
Test your understanding!
Write a function allocate_and_initialize that takes two arguments: an int ** (a double pointer) and an int value. The function should:
- Allocate memory on the heap for a single
int. - Check if the allocation was successful. If not, it should return
-1. - If successful, it should store the provided
intvalue in the newly allocated memory. - Make the caller's original pointer point to this new memory block.
- Return
0on success.
Show how you would call this function from main.
Show answer
Here is the implementation and the call from main.
#include <stdio.h>
#include <stdlib.h>
int allocate_and_initialize(int **ptr_addr, int value) {
// 1. Allocate memory for a single int
int *new_mem = malloc(sizeof(int));
// 2. Check for allocation failure
if (new_mem == NULL) {
return -1; // Failure
}
// 3. Store the value
*new_mem = value;
// 4. Modify the caller's pointer by dereferencing the double pointer
*ptr_addr = new_mem;
// 5. Return success
return 0;
}
int main() {
int *my_ptr = NULL; // Good practice to initialize pointers to NULL
printf("Before call: my_ptr = %p\n", my_ptr);
if (allocate_and_initialize(&my_ptr, 42) == 0) {
printf("After call: my_ptr = %p\n", my_ptr);
printf("Value pointed to by my_ptr: %d\n", *my_ptr);
// Don't forget to free the memory!
free(my_ptr);
my_ptr = NULL;
} else {
fprintf(stderr, "Memory allocation failed!\n");
}
return 0;
}
This demonstrates the full lifecycle: the caller's pointer my_ptr is modified by the function because we passed its address (&my_ptr).
An Elegant Use Case: Simplifying Linked List Removal
Double pointers aren't just for returning allocated memory; they can also lead to more elegant and efficient algorithms. One of the most famous examples comes from Linus Torvalds, the creator of Linux. He demonstrated how using a pointer-to-a-pointer can simplify the code for removing an element from a singly-linked list.
How do pointer-to-pointers work in C?
This final resource contains a legendary quote and explanation from Linus Torvalds. It's a masterclass in 'thinking with pointers'.
Read the answer by 'VonC', which contains a long quote from Linus. Focus on the two code snippets for removing a list entry: the 'obvious' way with a prev pointer and a special case for the head, and the 'clever' way using a pointer-to-a-pointer (**pp) that eliminates the special case. Try to trace the logic of the second version to see why it works for both the head and any other node.
The key idea is that the pointer you need to change is either the list_head itself (if removing the first element) or the next field of the previous element. A struct list_entry ** can point to either of these, allowing you to treat both cases with a single line of code: *pp = entry->next;. This is a powerful demonstration of how a deeper understanding of pointers leads to cleaner, more robust code.
Conclusion
Today we've added a crucial tool, the double pointer, to your C programming arsenal. You've seen that it's more than just a syntactic curiosity; it's the standard C idiom for allowing functions to modify pointers passed by a caller, and it can even lead to more elegant algorithms. Your experience in software architecture will help you appreciate how this feature enables the creation of modular and flexible C APIs.
Key Takeaways:
- A double pointer (
type **) is a variable that stores the memory address of another pointer. - It provides an additional level of indirection, requiring two dereferences (
**) to access the final value. - The primary use case is to pass a pointer to a function by reference, allowing the function to modify the caller's original pointer variable. This is essential for functions that allocate memory for the caller.
- Advanced use of double pointers, as seen in the linked-list example, can help eliminate special cases and make algorithms more generic and robust.
Preview of the Next Lesson:
We've seen that char **argv can be used to handle command-line arguments, which is effectively an array of strings. This syntax, where a double pointer is used with arrays, can sometimes be confusing. Is int **p the same as a 2D array int a[10][10]? (Hint: it's not). In the next lesson, we will dive into these nuances and learn to distinguish between a pointer to an array and an array of pointers, which is critical for correctly navigating multi-dimensional data structures in C.