Pointers to pointers (double indirection)
A pointer-to-pointer is exactly what the type says: a pointer whose target object is
itself a pointer. T * lets you reach a T; T ** lets you reach a T * so you can
read or replace the pointer value stored there. That extra level is what lets a function
change the caller's pointer, return allocated objects through out-parameters, and edit
linked data structures without special cases. The cost is cognitive: every * changes
which object you are talking about.
The reset:
int **ppdoes not point at anint. It points at anint *.*ppis the pointer object, and**ppis theintreached after following both levels.
The extra star changes what you can mutate
Start with one object and one pointer:
int value = 7;
int *p = &value;
int **pp = &p;
The levels are:
| Expression | Type | Object reached |
|---|---|---|
value |
int |
the integer object |
p |
int * |
a pointer object that stores &value |
*p |
int |
the integer reached through p |
pp |
int ** |
a pointer object that stores &p |
*pp |
int * |
the caller's pointer object p |
**pp |
int |
the integer reached through p |
Passing p to a function lets the function modify *p, the pointed-to int. Passing
&p lets the function modify p itself. That is the whole reason T ** shows up in C
APIs: the callee needs to replace a pointer variable owned by the caller.
How it really works
C passes arguments by value. If you call:
void set_to_null(int *p) {
p = NULL;
}
the assignment changes only the function's local copy of the pointer. The caller's pointer is untouched. To modify the caller's pointer object, pass its address:
void set_to_null(int **slot) {
*slot = NULL;
}
slot is a pointer to the caller's pointer variable. *slot = NULL writes into that
variable. The same pattern is used for allocation out-parameters:
bool make_widget(struct Widget **out);
The API says, "give me the address of your struct Widget *; if I succeed, I will store a
new owning pointer there." This is common when the return value is needed for status or
error reporting.
Double indirection is also a clean way to edit linked structures. In a singly linked list,
each "link" is a struct Node *: either the head pointer or a next field. A cursor of
type struct Node ** can point at whichever link currently leads to the place you want to
modify. Inserting at the head and inserting after a node become the same operation:
replace *link.
Arrays of pointers use the same syntax but a different idea. char **argv in main
points at the first element of an array of char * values. It is not a two-dimensional
character array; it is an array of pointers, and each pointer may point at a string stored
elsewhere. This distinction mirrors the earlier array lesson: T ** is not the same
layout as T rows[][cols].
Const gets noisy at this level because there are multiple things that can be const:
const char **a; // pointer to pointer to const char: can change *a
char * const *b; // pointer to const pointer to char: cannot change *b
const char * const *c; // pointer to const pointer to const char
Read from the inside out: what is the pointed-to object at each level, and which level is protected from writes?
Executable artifact: mutate the caller's pointer
The demo lives in examples/pointers-and-memory/pointers-to-pointers-double-indirection/demo.c.
#include <stdio.h>
#include <stdlib.h>
struct Node {
int value;
struct Node *next;
};
static int make_owned_int(int **out, int value) {
int *slot = malloc(sizeof *slot);
if (slot == NULL) {
*out = NULL;
return -1;
}
*slot = value;
*out = slot;
return 0;
}
static int push_front(struct Node **head, int value) {
struct Node *node = malloc(sizeof *node);
if (node == NULL) {
return -1;
}
node->value = value;
node->next = *head;
*head = node;
return 0;
}
static int append(struct Node **head, int value) {
while (*head != NULL) {
head = &(*head)->next;
}
return push_front(head, value);
}
static void print_list(const char *label, const struct Node *head) {
printf("%s", label);
for (const struct Node *node = head; node != NULL; node = node->next) {
printf(" %d", node->value);
}
printf("\n");
}
static void free_list(struct Node **head) {
while (*head != NULL) {
struct Node *victim = *head;
*head = victim->next;
free(victim);
}
}
int main(void) {
int value = 7;
int *p = &value;
int **pp = &p;
printf("value through p = %d\n", *p);
**pp = 42;
printf("value after **pp = %d\n", value);
int *owned = NULL;
if (make_owned_int(&owned, 99) != 0) {
perror("make_owned_int");
return 1;
}
printf("owned from out-param = %d\n", *owned);
free(owned);
owned = NULL;
struct Node *list = NULL;
if (append(&list, 10) != 0 ||
append(&list, 20) != 0 ||
append(&list, 30) != 0) {
free_list(&list);
perror("append");
return 1;
}
print_list("list after append =", list);
free_list(&list);
printf("list after free = %s\n", list == NULL ? "NULL" : "not NULL");
return 0;
}
Compile and run:
gcc -O0 -Wall -Wextra demo.c -o demo
./demo
Real output:
value through p = 7
value after **pp = 42
owned from out-param = 99
list after append = 10 20 30
list after free = NULL
**pp = 42 changes the original value through two levels. make_owned_int(&owned, 99)
stores a heap pointer into the caller's owned variable. The append function walks a
struct Node ** cursor until it points at the link that should be replaced, so appending
to an empty list and appending after the tail use the same write: *head = node.
Failure modes & trade-offs
- Losing track of the level.
p,*p, and**pname different objects. A wrong dereference can overwrite the pointer variable when you meant to overwrite the object, or the reverse. - Out-parameter ownership ambiguity.
T **outmust say whether the callee allocates, borrows, transfers ownership, or merely points into existing storage. The type alone does not say. - Partial initialization. If a function takes multiple out-parameters, define what happens on failure. Leaving some outputs written and others indeterminate is a cleanup trap.
T **vs multidimensional arrays.int **describes a pointer to pointer. It does not describe contiguousint rows[3][4]storage, whose decayed parameter type isint (*)[4].- Const holes. Converting between
T **andconst T **is not safe in the way many people expect; it can let code smuggle a non-const pointer to const storage. - Readable code cost. Double indirection can remove special cases in data structures, but beyond two levels the code usually needs a small helper type, clearer naming, or a different API.
In practice
- Use names that expose the level.
out,slot,link,headp, andcursorare clearer than another genericp. - Document out-parameter states. Say what the callee writes on success, what it writes on failure, and who owns the resulting pointer.
- Initialize outputs before risky work. Setting
*out = NULLearly makes cleanup paths simpler and safer. - Use
Node **linkfor singly linked list edits. It is one of the rare places where double indirection makes code smaller and more correct. - Keep pointer arrays and real 2D arrays separate.
char **argvstyle layouts are arrays of pointers; matrix-like contiguous storage should use pointer-to-array types. - Stop at two levels unless there is a strong reason.
T ***is sometimes real, but it should trigger an API design review.
Connects to: What a pointer really is · Pointer arithmetic & stride · The heap: malloc/free & the allocator underneath · Pointers & Memory · C from the Metal
Sources
- ISO/IEC 9899 (WG14 C standard working drafts) — pointer declarators, indirection, assignment, function-call argument passing, and qualification rules. https://www.open-std.org/jtc1/sc22/wg14/
- cppreference — Pointer declaration — compact reference for pointer-to-pointer syntax, null pointers, function parameters, and multi-level indirection. https://en.cppreference.com/w/c/language/pointer
- cppreference — Type qualifiers — how
constapplies at different pointer levels and why qualifier conversions get tricky withT **. https://en.cppreference.com/w/c/language/const - Jens Gustedt — Modern C — modern treatment of pointer parameters, out-parameters, ownership discipline, and linked data structures. https://gustedt.gitlabpages.inria.fr/modern-c/
- Richard Reese — Understanding and Using C Pointers — practical pointer-to-pointer examples: out-parameters, arrays of pointers, and linked list manipulation. https://www.oreilly.com/library/view/understanding-and-using/9781449344535/
- Beej's Guide to C — Pointers — approachable explanation of pointer levels,
argv, and passing pointers to functions. https://beej.us/guide/bgc/html/split/pointers.html