TRT
Sign in

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.

FreeAbout 16 minutes

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

Double indirection modifying memory through a pointer to a pointerstep 1 of 4
int val = 42;int *p = &val;int **pp = &p;**pp = 99;
  1. slot 0val (0x7ffd00): 42(looked at in this step)
  2. slot 1p (0x7ffd08): unset
  3. slot 2pp (0x7ffd10): unset
  4. 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:

c
#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.

text
Stack Memory Layout
Address:      0x7ffd10          0x7ffd08          0x7ffd00
Variable:     pp                p                 val
Content:    [ 0x7ffd08 ]  ──► [ 0x7ffd00 ]  ──► [   42     ]
Type:         int **            int *             int

In the trace above:

  1. val occupies 4 bytes at 0x7ffd00 and stores 42.
  2. p occupies 8 bytes at 0x7ffd08 and stores 0x7ffd00 (the address of val).
  3. pp occupies 8 bytes at 0x7ffd10 and stores 0x7ffd08 (the address of p).

1. Dereferencing Levels

Each asterisk (*) strips away one layer of indirection:

  • pp: Evaluates to 0x7ffd08 (the address of pointer p).
  • *pp: Dereferences pp once, reading the contents of p. Evaluates to 0x7ffd00 (the address of val).
  • **pp: Dereferences twice. Reads p, follows it to val, and accesses the integer itself. In step 4 of the trace, **pp = 99 writes 99 into val.

2. Fixing the Pointer-Reassignment Bug

Using double indirection, we can update the caller's pointer:

c
#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():

c
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.

text
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:

c
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 CallerPassed ArgumentParameter Type in FunctionDereference to Modify
int x&xint *p*p = 10;
float f&ffloat *p*p = 3.14f;
int *ptr&ptrint **pp*pp = new_address;
void *raw&rawvoid **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:

OperationMemory ReadsTypical LatencyImpact
Direct read (val)1 read1–4 cyclesValue loaded from register or L1 cache
Single indirection (*p)2 reads4–8 cyclesLoads pointer, then loads target value
Double indirection (**pp)3 reads8–15 cyclesLoads pp, loads p, then loads target value
Memory pointer array tableN x 8 bytesExtra RAM8 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:

c
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 a const int. The integer cannot be modified through **pp, but *pp and pp can be reassigned.
  • int * const *pp: Pointer to a const pointer to an int. The pointer *pp cannot be reassigned, but the integer **pp can be modified.
  • int ** const pp: const pointer to a pointer to an int. pp itself 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

Not started

Sign in to save your learning progress.

Sign in to save