C in 100 Seconds: Double Pointers | Episode 29
Video: C in 100 Seconds: Double Pointers — Pointer to Pointer | Episode 29 by Taught by Celeste AI - AI Coding Coach
C Double Pointers: Pointer to Pointer
int **pp = &p;—pppoints to a pointer that points to an int. Used to modify a pointer through a function, or for arrays of arrays.
A pointer to a pointer. Sounds esoteric — but it's the only way to make a function modify the caller's pointer (not just the value the pointer points to). Used in linked-list head modification, array-of-strings allocation, and char **argv.
The basic shape
#include <stdio.h>
#include <stdlib.h>
void allocate(int **ptr, int value) {
*ptr = malloc(sizeof(int));
**ptr = value;
}
int main() {
int x = 42;
int *p = &x;
int **pp = &p;
printf("x = %d\n", x); // 42
printf("*p = %d\n", *p); // 42
printf("**pp = %d\n", **pp); // 42
int *data = NULL;
allocate(&data, 99);
printf("\nallocated: %d\n", *data); // 99
free(data);
return 0;
}
Three levels of dereferencing
int x = 42;
int *p = &x; // p holds the address of x
int **pp = &p; // pp holds the address of p
x // 42 — direct
*p // 42 — dereference p once
**pp // 42 — dereference pp twice
Each * peels one layer of indirection. **pp says "go to pp, follow that to p, follow that to x."
Why use double pointers?
The key use: modify the caller's pointer.
void allocate(int **ptr, int value) {
*ptr = malloc(sizeof(int)); // modify the caller's pointer
**ptr = value; // modify the int it points to
}
int *data = NULL;
allocate(&data, 99);
// data now points to malloc'd memory holding 99
Without **, the function would only modify a local copy:
void no_allocate(int *ptr, int value) {
ptr = malloc(sizeof(int)); // modifies local ptr only
*ptr = value; // writes to malloc'd memory; original is leaked
}
int *data = NULL;
no_allocate(data, 99);
// data is still NULL — function couldn't modify it
int *ptr parameter is a copy of the caller's pointer. To modify the original, take its address: pass &data.
Linked list head update
The classic use:
typedef struct Node {
int value;
struct Node *next;
} Node;
void prepend(Node **head, int value) {
Node *new = malloc(sizeof(Node));
new->value = value;
new->next = *head;
*head = new; // modifies caller's `head`
}
Node *head = NULL;
prepend(&head, 1);
prepend(&head, 2);
prepend(&head, 3);
// head now points to {3, {2, {1, NULL}}}
When you prepend, the head pointer itself changes. Without **, the caller's head wouldn't update.
char **argv in main
int main(int argc, char **argv) {
for (int i = 0; i < argc; i++) {
printf("%s\n", argv[i]);
}
}
argv is "pointer to (pointer to char)" — each element of argv is a char * pointing to one argument string. Indexing once: argv[i] is a char *. Dereferencing: *argv[i] is the first char of that string.
char *argv[] and char **argv are equivalent in function parameters (arrays decay to pointers).
Dynamic 2D arrays
int rows = 3, cols = 4;
int **grid = malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
grid[i] = malloc(cols * sizeof(int));
}
grid[1][2] = 42;
// free in reverse order
for (int i = 0; i < rows; i++) free(grid[i]);
free(grid);
Two-step allocation: first the array of pointers, then each row. Frees mirror it.
For contiguous 2D, a single allocation is cheaper:
int *grid = malloc(rows * cols * sizeof(int));
grid[i * cols + j] = 42; // manual indexing
Better cache locality; only one malloc/free.
When to stop adding asterisks
Theoretically you can have int ***p and beyond. Practically, more than two levels gets confusing.
Common cases:
int *p— simple pointer.int **pp— function modifying a pointer; array of pointers.int ***ppp— modifying anint **from a function (rare but possible).
If you find yourself writing ***, consider whether a struct would be clearer.
Common mistakes
* instead of **. Forgetting one level of indirection. Compiler usually catches.
Forgetting & at call site. allocate(data, 99) — passes data's value, not address. Function can't modify caller's data.
Walking **pp without checking pointers. If *pp is NULL, **pp crashes.
Confusing int *p[] and int (*p)[]. First is "array of pointers to int" (episode 28). Second is "pointer to array of int." Different.
Memory leaks in 2D allocation. Forget to free each row before freeing the outer array. Walk it correctly.
Visualising
int x = 42;
int *p = &x;
int **pp = &p;
Memory:
+---+ +-----+ +-----+ +-----+
| x | <-| p | <-| pp | | |
| 42| |0xA | |0xB | | |
+---+ +-----+ +-----+ +-----+
0xA 0xB 0xC
x = 42
p = 0xA (address of x)
pp = 0xB (address of p)
*pp reads what's at 0xB → 0xA (the value of p). **pp dereferences again → 42.
What's next
Episode 30: void pointers. Generic pointers with no type. Used for qsort, generic data structures.
Recap
int **pp is "pointer to pointer to int." Use to modify a pointer from inside a function (linked-list head updates, dynamic allocation), for arrays of arrays, and for char **argv. Pass with &p; access with **pp. For two-level allocations, allocate the outer array first, then each row; free in reverse. Stop at two levels — ***p is usually a code smell.
Next episode: void pointers.