Foundations · Module 3 · Lesson 7 of 9
Pointers to Pointers and Indirection
Master multi-level pointer indirection, modify caller pointer addresses across function frames, and work with pointer tables.
By the end you can
- Trace multiple levels of pointer dereferencing through virtual memory frames
- Pass double pointers to functions to reassign caller pointer targets
- Work with pointer tables such as command-line argument arrays
- Differentiate pointer-to-pointer tables from contiguous 2D array blocks
Builds on C-Style Strings and Buffer Bounds.
01 · A question first
Why does passing a pointer to a function that changes its target leave the caller's pointer unchanged? Understanding double indirection reveals how pass-by-value applies to memory addresses.
Make a guess, then step through the trace below to check it.
02 · Watch it run
int val = 42;int *p = &val;int **pp = &p;**pp = 99;- slot 0val (0x7ffd00): 42(looked at in this step)
- slot 1p (0x7ffd08): unset
- slot 2pp (0x7ffd10): unset
- slot 3deref: none
Stack allocates 4 bytes for integer val at address 0x7ffd00, storing value 42.
Machine work: movl $42, -4(%rbp)
03 · The idea
Start with the problem
A standard task in C is writing a helper function that redirects a caller's pointer. A developer writes the following code:
#include <stdio.h>
void redirect(int *ptr, int *target) {
ptr = target;
}
int main(void) {
int first = 10;
int second = 42;
int *data = &first;
redirect(data, &second);
printf("%d\n", *data); /* Still prints 10 */
return 0;
}When run, the program prints 10, not 42.
Why did assigning a new address to ptr have zero effect on data in main()?
The answer comes down to C parameter passing semantics: every function argument in C is passed by value. When you pass data, the function receives a local copy of the pointer. Changing that local copy changes only the local stack frame.
To modify an integer variable in a caller, you pass a pointer to that integer (int *). By identical logic, to modify a pointer variable in a caller, you must pass a pointer to that pointer (int **).
How the idea works
A pointer to a pointer (often called a double pointer) is a variable that stores the virtual memory address of another pointer variable.
The extra level does not make the original value dynamic or special. It simply gives a function access to the caller's pointer variable, so that function can read or replace the address stored there.
Stack Memory Layout
Address: 0x7ffd10 0x7ffd08 0x7ffd00
Variable: pp p val
Content: [ 0x7ffd08 ] ──► [ 0x7ffd00 ] ──► [ 42 ]
Type: int ** int * intIn the trace above:
valoccupies 4 bytes at0x7ffd00and stores42.poccupies 8 bytes at0x7ffd08and stores0x7ffd00(the address ofval).ppoccupies 8 bytes at0x7ffd10and stores0x7ffd08(the address ofp).
1. Dereferencing Levels
Each asterisk (*) strips away one layer of indirection:
pp: Evaluates to0x7ffd08(the address of pointerp).*pp: Dereferencespponce, reading the contents ofp. Evaluates to0x7ffd00(the address ofval).**pp: Dereferences twice. Readsp, follows it toval, and accesses the integer itself. In step 4 of the trace,**pp = 99writes99intoval.
2. Fixing the Pointer-Reassignment Bug
Using double indirection, we can update the caller's pointer:
#include <stdio.h>
void redirect(int **ptr, int *target) {
*ptr = target;
}
int main(void) {
int first = 10;
int second = 42;
int *data = &first;
redirect(&data, &second); /* Pass address of pointer */
printf("%d\n", *data); /* Prints 42 */
return 0;
}By passing &data, ptr receives the stack address of data. The assignment *ptr = target dereferences that address and writes the address of second directly into data inside main().
3. Pointer Arrays and Command-Line Arguments
The most common real-world double pointer is the second parameter of main():
int main(int argc, char *argv[])In function parameter lists, array syntax decays to a pointer. Thus, char *argv[] is identical to char **argv: a pointer to an array of pointers, where each pointer targets a null-terminated string.
argv (char **)
│
▼
[ argv[0] ] ──► "/usr/bin/app\0"
[ argv[1] ] ──► "--verbose\0"
[ argv[2] ] ──► "input.txt\0"
[ NULL ]You can iterate through arguments using pointer arithmetic on the double pointer:
void print_args(char **argv) {
while (*argv != NULL) {
printf("Arg: %s\n", *argv);
argv++; /* Advances by sizeof(char *) = 8 bytes */
}
}Why it is correct
Double pointers enforce a single, unified memory model across all types in C.
Uniformity of Pass-by-Value
C has no special "reference" mechanism. The rules for modifying variables across function calls are uniform across all data types:
| Variable Type in Caller | Passed Argument | Parameter Type in Function | Dereference to Modify |
|---|---|---|---|
int x | &x | int *p | *p = 10; |
float f | &f | float *p | *p = 3.14f; |
int *ptr | &ptr | int **pp | *pp = new_address; |
void *raw | &raw | void **pp | *pp = new_address; |
Applying pointer semantics to pointers themselves ensures that addresses can be mutated using the same mechanics as primitive integers.
What it costs
Using multiple indirection levels introduces specific performance implications:
| Operation | Memory Reads | Typical Latency | Impact |
|---|---|---|---|
Direct read (val) | 1 read | 1–4 cycles | Value loaded from register or L1 cache |
Single indirection (*p) | 2 reads | 4–8 cycles | Loads pointer, then loads target value |
Double indirection (**pp) | 3 reads | 8–15 cycles | Loads pp, loads p, then loads target value |
| Memory pointer array table | N x 8 bytes | Extra RAM | 8 bytes per pointer plus separate target buffers |
Pointer Chasing and Cache Misses
Each additional level of indirection requires a separate memory load. If pp, p, and val reside in distant parts of virtual memory, each dereference can trigger an independent CPU cache miss. For performance-critical data pathways, flat contiguous buffers outperform nested pointer structures.
Pitfalls and variations
Pitfall 1: Confusing Pointer Tables with Contiguous Arrays
A pointer table stores addresses, not the pointed-to objects themselves:
char first[] = "first";
char second[] = "second";
char *words[2] = {first, second};words contains two pointers, while first and second contain the characters. The pointer table and the character arrays are separate objects, so changing words[0] changes which string the table selects; it does not copy or modify the characters.
Pitfall 2: const Placement with Double Pointers
Adding const to double pointers requires precision:
const int **pp: Pointer to a pointer to aconst int. The integer cannot be modified through**pp, but*ppandppcan be reassigned.int * const *pp: Pointer to aconstpointer to anint. The pointer*ppcannot be reassigned, but the integer**ppcan be modified.int ** const pp:constpointer to a pointer to anint.ppitself cannot be reassigned.
Where it is used
Double pointers are fundamental across systems architecture:
- Pointer-table updates: A function can change which existing string a caller's table entry selects by accepting
char **. - Git and File Path Trees: Directory trees and command parsers pass
char **streams to tokenize arguments and traverse path hierarchies.
Dynamic allocation and linked-list ownership are covered later in the C track. In the next lesson, we will explore pointers that target executable machine code rather than passive data: mastering function pointers and callback architecture.
04 · Prove it
Question 1 of 3 · answer from memory
Why must a function that changes the target of a caller's pointer take a double pointer (e.g., void redirect(int **ptr))?
05 · Practice
Something wrong with this lesson? Report a problem