Welcome back! In our previous lesson, we established what double pointers are and how to trace multi-level dereferencing by following the chain of memory addresses.
Today, we will focus on one of the most critical and practical applications of this concept, which is directly tied to your goal of mastering interview topics. We'll explore why double pointers are essential for writing functions that can modify a pointer variable belonging to the caller. This pattern is fundamental in C for managing dynamic resources, a common task in the low-level systems programming you do.
Your learning outcome for this lesson is to use a double pointer to modify a pointer variable from within a function (e.g., reassigning a pointer passed from a caller).
The Core Problem: C is Strictly Pass-by-Value
A common source of bugs and a frequent interview question revolves around a function's ability (or inability) to change its arguments. The rule in C is simple and absolute: all function arguments are passed by value.
When you pass a variable to a function, a copy of that variable's value is created on the function's stack frame. Any modifications made to the parameter inside the function affect only this local copy.
This is straightforward for a simple type like int. To modify an int in the calling scope, you don't pass the int itself; you pass a pointer to it (int *).
void modify_int(int *ptr) {
*ptr = 100; // Modifies the original int
}
int main() {
int x = 10;
modify_int(&x); // x is now 100
return 0;
}
But what happens when the variable you want to modify is itself a pointer? Let's try passing it "by value" and see what happens.
#include <stdio.h>
#include <stdlib.h>
void failed_allocation(int *p) {
p = malloc(sizeof(int)); // Allocate memory
*p = 42;
printf("Inside function: p points to address %p, value is %d\n", (void*)p, *p);
}
int main() {
int *ptr = NULL;
failed_allocation(ptr);
if (ptr == NULL) {
printf("Outside function: ptr is still NULL!\n");
} else {
printf("Outside function: ptr points to address %p\n", (void*)ptr);
// The line below would cause a crash (dereferencing NULL)!
// printf("Value is %d\n", *ptr);
}
return 0;
}
When you run this, you'll see that ptr in main remains NULL. The memory allocated by malloc is leaked because the only pointer to it, the local variable p in failed_allocation, was destroyed when the function returned.
The assignment p = malloc(...) only changed the local copy of the pointer. The original ptr in main was unaffected.
Function parameter stack copy illustration
This diagram illustrates why passing a pointer by value fails to modify the original. The argument str_arg from main is copied into the parameter str_par on the function's stack. Modifying str_par to point to a new location only affects the copy, not the original str_arg.
The Solution: Pass the Address of the Pointer
To modify the caller's pointer, you must apply the same logic as for modifying an int: you need to pass its address. The address of a pointer (int *) has the type "pointer to a pointer to an int" (int **).
By passing a double pointer, the function receives the address of the original pointer. It can then dereference this address to access and modify the original pointer in the calling scope.
Let's fix our previous example.
#include <stdio.h>
#include <stdlib.h>
void successful_allocation(int **pp) { // Note the double pointer
*pp = malloc(sizeof(int)); // Modify the original pointer
**pp = 42;
printf("Inside function: *pp points to %p, value is %d\n", (void*)*pp, **pp);
}
int main() {
int *ptr = NULL;
successful_allocation(&ptr); // Pass the address of the pointer
if (ptr == NULL) {
printf("Outside function: ptr is still NULL.\n");
} else {
printf("Outside function: ptr points to %p, value is %d\n", (void*)ptr, *ptr);
free(ptr); // We must now free the allocated memory
}
return 0;
}
Let's trace successful_allocation(&ptr):
- In
main,ptris anint *variable. &ptrtakes its address. The type isint **. This address is passed to the function.- The function parameter
pp(anint **) now holds the memory address ofmain'sptr. *pp = malloc(...)does the following:- It dereferences
pponce, which gives access to the originalptrvariable inmain. - It assigns the new memory address returned by
mallocto that originalptr.
- It dereferences
- When the function returns,
ptrinmainnow correctly points to the newly allocated memory.
Diving Deeper with Code Examples
This pattern is extremely common. To solidify your understanding, let's look at a few curated examples that really drive the point home.
Why use double indirection? or Why use pointers to pointers?
The Stack Overflow discussion 'Why use double indirection?' provides excellent, practical code examples that contrast the failed single-pointer approach with the successful double-pointer one. These are perfect for sharpening your reasoning.
First, read the answer that provides the cant_change and change functions. Pay close attention to the code and the annotated output to see the effect inside and outside the function. Then, study the next answer which shows the alloc1 and alloc2 functions. This directly mirrors our example and is a critical use case to understand for interviews.
The resource you just reviewed highlights two key scenarios: re-seating a pointer to point to a different existing variable, and pointing it to newly allocated memory. Both scenarios fail without a double pointer because of C's pass-by-value semantics.
Application: Modifying a Linked List Head
Another classic use case is modifying the head pointer of a linked list. If you have a function to insert a new node at the beginning of the list, that function must be able to change the head pointer itself, especially if the list is initially empty.
Passing a double pointer in a function - Stack Overflow
This Stack Overflow thread provides a perfect context for our discussion, rooted in a common linked-list problem. The answers clearly explain the reasoning.
First, read the answer from 'Some programmer dude' (sections 1 and 2), which provides a concise explanation of pass-by-value and a clear change1 vs. change2 example. Then, look at the answer from 'CiaPan' (section 4), which shows how a function like append_at_end would correctly pass its double-pointer argument to insert_at_head.
As you saw, a function like insert_at_head(struct node **head) needs a double pointer because if the list is empty (*head == NULL), it must allocate a new node and make the original head pointer in the calling function point to it.
A subtle but interesting point from the linked-list question was whether to call the function with head or &(*head). As one of the answers noted (citing the C standard), if head is already a double pointer (struct node **), then &(*head) is equivalent to head. The & and * operators cancel each other out. This is a great detail to have in your back pocket for an interview.
Practice Exercise
Let's put this into practice. Imagine you're writing a utility function for a communications module. This function needs to create a formatted diagnostic message string. The function should allocate the necessary memory and return the new string to the caller via an output parameter.
Your task is to complete the create_diagnostic_message function.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// TODO: Implement this function.
// It should allocate a string and format a message into it.
// It should return 0 on success, and -1 on failure (e.g., malloc fails).
// The caller is responsible for freeing the memory.
int create_diagnostic_message(char **msg_ptr, int error_code, const char *module) {
// Your implementation here
}
int main(void) {
char *diagnostic_msg = NULL;
int error = 0;
error = create_diagnostic_message(&diagnostic_msg, 503, "CAN_Driver");
if (error == 0 && diagnostic_msg != NULL) {
printf("Generated Message: %s\n", diagnostic_msg);
free(diagnostic_msg); // Clean up
} else {
printf("Failed to create message.\n");
}
return 0;
}
Solution and Explanation
Here is a possible implementation for create_diagnostic_message.
int create_diagnostic_message(char **msg_ptr, int error_code, const char *module) {
// 1. Check for invalid input pointer.
if (msg_ptr == NULL) {
return -1;
}
// 2. Calculate the required buffer size. A safe size for the message.
// "Error: %d in module: %s" plus space for numbers and string.
// snprintf can tell us the size needed, but for simplicity we'll estimate.
const char *format = "Error: %d in module: %s";
int len = snprintf(NULL, 0, format, error_code, module);
if (len < 0) {
return -1; // Encoding error
}
// 3. Allocate memory. Size is len + 1 for the null terminator.
*msg_ptr = malloc(len + 1);
if (*msg_ptr == NULL) {
return -1; // Malloc failed
}
// 4. Format the string into the newly allocated buffer.
sprintf(*msg_ptr, format, error_code, module);
return 0; // Success
}
Explanation:
- The function accepts
char **msg_ptrto be able to modify the caller'schar *diagnostic_msg. - We use
*msg_ptr = malloc(...)to assign the newly allocated memory address back todiagnostic_msginmain. - All subsequent operations, like
sprintf, also use*msg_ptrto refer to the allocated buffer. - By passing
&diagnostic_msgfrommain, we provide the function with the means to change wherediagnostic_msgpoints.
Conclusion
Today we've tackled a concept that is a frequent point of confusion in C, but is incredibly powerful once mastered. By understanding that C is always pass-by-value, the need for an extra level of indirection (**) to modify a pointer argument becomes clear.
Key Takeaways:
- C always passes function arguments by value, meaning the function gets a copy of the argument.
- To modify a variable in the calling function, you must pass its address.
- To modify a pointer variable (
type *), you must pass its address, resulting in a double pointer (type **). - Inside the function, dereferencing the double pointer (
*pp) gives you access to the original pointer from the calling scope, allowing you to reassign it (e.g., to memory frommallocor to another address).
In our next lesson, we will explore another common use of the ** syntax: declaring and using an array of pointers, as famously seen in char *argv[]. This will further broaden your understanding of how pointers can be used to create flexible and efficient data structures.