Question
When should a pointer to a pointer (double indirection) be used in C? I understand that int **p is a pointer to another pointer, but why would a program need this level of indirection? Please explain with a practical example.
Short Answer
A pointer to a pointer is useful when a function must change the caller’s pointer itself, not only the value it points to. This page explains double indirection in C, including pointer allocation, output parameters, linked-list updates, and the important difference between int ** and a true two-dimensional array.
Concept
In C, a pointer stores the address of another object. A pointer to a pointer stores the address of a pointer.
int value = 42;
int *ptr = &value;
int **ptr_to_ptr = &ptr;
The relationships are:
valueis anintcontaining42.ptrcontains the address ofvalue.ptr_to_ptrcontains the address ofptr.
Dereferencing follows one address at a time:
*ptr // value, which is 42
*ptr_to_ptr // ptr, an int *
**ptr_to_ptr // value, which is 42
The key reason for using T ** is that C passes function arguments by value. If you pass a T * to a function, the function receives a copy of that pointer. It can modify the pointed-to object, but assigning a new address to its local pointer does not update the caller’s pointer.
Pass the address of the pointer—using T **—when the function needs to replace, allocate, clear, or otherwise update the caller’s pointer variable.
This matters in real programs because many operations naturally need to update an address: allocating memory, returning a newly created object, inserting at the beginning of a linked list, or receiving an array of strings from an API.
Mental Model
Think of a pointer as a note containing a house address.
- A value is the item inside the house.
- A pointer is a note that tells you which house contains the item.
- A pointer to a pointer is the location of the note itself.
If a function only needs to change something in the house, give it the address note (T *).
If the function needs to replace the address written on the note—for example, point it at a newly built house—give it the location of the note (T **). That lets it rewrite the caller’s pointer.
Syntax and Examples
The general declaration is:
Type **name;
For example:
int number = 10;
int *first = &number;
int **second = &first;
printf("%d\n", number); // 10
printf("%d\n", *first); // 10
printf("%d\n", **second); // 10
second points to first, and first points to number.
Updating a caller's pointer
This function allocates an integer and stores its address in the caller’s pointer:
#include <stdio.h>
#include <stdlib.h>
int create_number(int **out_number) {
*out_number = malloc(sizeof **out_number);
if (*out_number == NULL) {
return 0;
}
**out_number = 99;
return 1;
}
int main(void) {
int *number = NULL;
if (create_number(&number)) {
printf("%d\n", *number); // 99
free(number);
}
return 0;
}
number is an int *. The expression &number is its address, so its type is int **. Inside create_number:
*out_numbermeansnumberinmain.**out_numbermeans the allocatedintvalue.
Using sizeof **out_number is a robust allocation style: it allocates space for the final pointed-to object, an int in this example.
Step by Step Execution
Trace this small example:
#include <stdio.h>
void reset_pointer(int **target) {
*target = NULL;
}
int main(void) {
int value = 7;
int *pointer = &value;
reset_pointer(&pointer);
printf("%p\n", (void *)pointer);
return 0;
}
valuestores7.pointerstores the address ofvalue.&pointeris the address of the pointer variable itself. Its type isint **.reset_pointerreceives that address intarget.*targetaccesses the originalpointervariable inmain.- Assigning
NULLto*targetchangespointerinmain. - The printed pointer value is a null pointer, commonly displayed as
(nil)or0x0; the exact formatting is implementation-dependent.
By contrast, this does not reset the caller’s pointer:
void does_not_reset(int *target) {
target = NULL; // Changes only the local copy.
}
Here, target is a copied pointer value. Reassigning it has no effect outside the function.
Real World Use Cases
Common uses of pointers to pointers in C include:
- Memory allocation helpers: A function calls
mallocand returns the allocated address through an output parameter. - Updating a linked-list head: Inserting or deleting the first node changes the head pointer.
- APIs with output parameters: A library function may create an object and write its address into a caller-provided pointer variable.
- Arrays of strings: Command-line arguments use
char **argv; each element is achar *pointing to a string. - Dynamically allocated table representations: An array of row pointers can be represented by
int **rows, where eachrows[i]points to a row.
For example, inserting a node at the start of a singly linked list must change the caller’s head pointer:
struct Node {
int value;
struct Node *next;
};
void prepend(struct Node **head, struct Node *node) {
node->next = *head;
*head = node;
}
Real Codebase Usage
In production C code, T ** commonly appears in APIs that return or replace an owned resource.
Output parameters with error handling
A function can report success with a return value and provide a created object through an output parameter:
#include <stdlib.h>
int make_buffer(size_t size, unsigned char **out_buffer) {
if (out_buffer == NULL) {
return 0;
}
*out_buffer = NULL; // Safe state on failure.
if (size == 0) {
return 1;
}
*out_buffer = malloc(size);
return *out_buffer != NULL;
}
This pattern has useful properties:
- The function validates the output location before dereferencing it.
- It initializes
*out_buffertoNULL, so failure does not leave an unspecified pointer behind. - The caller owns the allocation on success and must call
free.
Linked-list modifications
Rather than handling the head node as a special case, code can use a pointer-to-pointer cursor:
void remove_value(struct Node **head, int value) {
struct Node **link = head;
while (*link != NULL) {
if ((*link)->value == value) {
struct Node *removed = *link;
*link = removed->next;
free(removed);
return;
}
link = &(*link)->next;
}
}
link always points to the pointer that leads to the current node: either head or a previous node’s next field. Therefore the same assignment, *link = removed->next, works for both head removal and middle removal.
Common Mistakes
Passing pointer instead of &pointer
If the parameter type is int **, pass the address of an int * variable.
int *number = NULL;
create_number(&number); // Correct
This is wrong and should produce a compiler diagnostic:
create_number(number); // Wrong: number has type int *, not int **
Dereferencing before checking for NULL
Both pointer levels may need validation.
void set_zero(int **out) {
**out = 0; // Unsafe if out or *out is NULL.
}
Check the levels needed by your operation:
if (out != NULL && *out != NULL) {
**out = 0;
}
Forgetting that malloc can fail
Never write through an allocation result until you check it.
*result = malloc(sizeof **result);
if (*result == NULL) {
return 0;
}
Confusing int ** with a two-dimensional array
These types are not interchangeable:
int matrix[3][4];
A 3 x 4 array is stored as one contiguous block and, when passed to a function, needs a parameter with the column size known:
void print_matrix(int rows, int matrix[][4]);
An int ** is instead a pointer to pointers, often used for separately allocated rows. Casting int (*)[4] or int[3][4] data to int ** is undefined behavior.
Using casts to hide incompatible pointer types
Do not force a type match with a cast such as (int **)matrix. Fix the declaration and function parameter so they describe the actual memory layout.
Losing an allocated pointer
If a function overwrites a pointer that already owns memory, the old allocation can become unreachable. Free or deliberately transfer ownership of the old allocation before replacing the pointer.
Comparisons
| Situation | Suitable type | Why |
|---|---|---|
| Read or change one integer | int * | The function needs the integer’s address. |
| Allocate an integer and update the caller’s pointer | int ** | The function must write a new address into the caller’s int *. |
| Traverse command-line strings | char **argv | argv points to elements that are each char *. |
| Fixed-width contiguous 2D array | int matrix[][COLS] or int (*matrix)[COLS] | The compiler needs the row width to compute indexes. |
| Independently allocated rows | int **rows | Each row address is stored separately. |
A helpful rule is: use one * for each level of object or pointer you must reach.
- To modify a
Tthrough its address: useT *. - To modify a
T *through its address: useT **. - To modify a
T **through its address: useT ***.
More indirection is not automatically better; use it only when the data structure or required update genuinely has another pointer level.
Cheat Sheet
T value;
T *ptr = &value; // Pointer to T
T **ptr_ptr = &ptr; // Pointer to pointer to T
*ptr_ptr // A T *
**ptr_ptr // A T
void replace(T **out) {
*out = /* a T * value */;
}
T *item = NULL;
replace(&item);
Key rules:
- Use
T **when a function must modify a caller-ownedT *variable. - Pass
&pointerto aT **parameter. *outaccesses the caller’s pointer;**outaccesses the final object.- Check
out != NULLbefore evaluating*out. - If allocating, check
*out != NULLbefore dereferencing it. - A pointer-to-pointer is not automatically a 2D array.
- Use
freeexactly once for each successful allocation, according to the ownership rules of the API.
FAQ
Why does C need pointers to pointers?
C function parameters are passed by value. A pointer to a pointer lets a function access and change the caller’s pointer variable.
Can I return a pointer instead of using T **?
Yes. For simple allocation, returning T * is often clear. A T ** output parameter is useful when the return value is needed for success or error status, or when an API follows an output-parameter convention.
Does **p always mean a pointer to a pointer?
In an expression, **p means dereference p twice. In a declaration, such as int **p, it declares p as a pointer to an int *.
Is char **argv a pointer to a pointer?
Yes. Each argv[i] is a char * pointing to a command-line string, and argv points to those char * elements.
Is int ** the same as int matrix[rows][cols]?
No. An int ** usually describes separately stored row pointers. A true multidimensional array is contiguous and has a different type and indexing layout.
Should I use a cast with malloc in C?
No. In C, assign malloc directly to the correctly typed pointer. A cast can hide a missing declaration for malloc and does not fix incorrect pointer types.
Can a pointer to a pointer be NULL?
Yes. A T ** can be NULL, and it can also point to a T * whose value is NULL. These are different states, so validate the level you will dereference.
Mini Project
Description
Create a small singly linked-list utility that can add values at the front and remove a value. This demonstrates a practical reason for struct Node **: both operations may need to replace the list’s head pointer.
Goal
Build a list program that updates the head pointer safely through a pointer-to-pointer parameter.
Requirements
- Define a
struct Nodewith an integer value and anextpointer. - Write a function that prepends a value to the list.
- Write a function that removes the first node containing a requested value.
- Print the list before and after removal.
- Free every remaining node before the program exits.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.