C / POINTERS
Pointers to pointers and multi-level indirection
Use int ** to reach and modify a pointer itself: read multi-level declarations, pass &ptr to functions that allocate or repoint, and walk arrays of pointers.
What you will learn
- Read int **pp as: *pp is a pointer, **pp is the int itself
- Pass &ptr so a callee can allocate, free or repoint the caller's own pointer
- Distinguish *pp = q (changes a pointer) from **pp = 5 (changes a value)
- Walk an array of pointers with char ** and explain why it is not a 2D array
Understanding Pointers to pointers and multi-level indirection
A pointer variable occupies storage like any other object, so it has an address of its own, and &p is a perfectly ordinary expression whose type is int **. Read the declaration as a promise about dereferencing: int **pp says that after two applications of * you are looking at an int, so *pp has type int * and **pp has type int. Nothing new happens at the machine level; each * is one more load from memory, and multi-level indirection is just that one operation repeated.
The reason T ** appears in real code is that C passes every argument by value, pointers included. A function that receives an int * gets a copy of the address, so it can change the int that was pointed at, but it can never change which int the caller's variable points to. To do that it needs the address of the caller's pointer variable, which is the same reasoning that makes you pass &n when a function must change an int n, applied one level up. That is why allocation helpers, destroy functions and linked-list rewiring take T **.
It helps to draw the chain and to remember that each link is a separate object with its own lifetime. pp being non-null tells you nothing about whether *pp is non-null, so a two-level dereference needs two checks or a documented invariant. And char ** is not a two-dimensional array: it points at a single char *, possibly the first of an array of them, each of which may live anywhere, whereas char grid[3][6] is one contiguous block that stores no addresses at all. The two types are not interchangeable, and casting between them makes the code read stored characters as if they were an address.
<stdio.h>
int main(void)
{
int value = 42;
int *p = &value;
int **pp = &p;
printf("value = %d\n", value);
printf("*p = %d\n", *p);
printf("**pp = %d\n", **pp);
printf("*pp == p : %d\n", *pp == p);
printf("&p == pp : %d\n", &p == pp);
**pp = 7; /* two levels: reaches the int */
printf("after **pp = 7, value = %d\n", value);
*pp = NULL; /* one level: reaches the pointer p */
printf("after *pp = NULL, p is null: %d\n", p == NULL);
printf("value survives: %d\n", value);
return 0;
}
A pointer is itself an addressable object, so T ** is merely a pointer to a pointer, and the level at which you assign decides whether you change a pointer or the value it points to.
Worked examples
Allocating into the caller's pointer
A function returns a status code and delivers the new block through an int ** out-parameter, and a matching release function clears the caller's pointer.
<stdio.h>
<stdlib.h>
/* Returns 1 on success and stores the block in *out. */
static int make_squares(int **out, int n)
{
int *a = malloc((size_t)n * sizeof *a);
int i;
if (a == NULL)
return 0;
for (i = 0; i < n; i++)
a[i] = i * i;
*out = a;
return 1;
}
static void release(int **p)
{
free(*p);
*p = NULL;
}
int main(void)
{
int *squares = NULL;
int i;
if (!make_squares(&squares, 5)) {
puts("out of memory");
return 1;
}
for (i = 0; i < 5; i++)
printf("%s%d", i ? "," : "", squares[i]);
putchar('\n');
release(&squares);
printf("after release: %s\n", squares == NULL ? "NULL" : "dangling");
return 0;
}
Example explained
Line 1*out = a; writes through the parameter into the caller's squares variable; writing out = a; would only overwrite the local copy of the address.
Line 2The pointer travels out through the parameter precisely so the return value is free to carry success or failure.
Line 3release takes int ** so it can both free(*p) and set *p = NULL; a release(int *p) could free the block but could never clear the caller's variable.
Line 4sizeof *a in the malloc call ties the element size to the declared type of a rather than repeating int.
An array of pointers seen through char **
Swapping two entries rearranges addresses without touching any characters, and a char ** cursor walks the array one pointer at a time.
<stdio.h>
static void swap_ptr(char **a, char **b)
{
char *tmp = *a;
*a = *b;
*b = tmp;
}
int main(void)
{
char *words[3] = { "birch", "ash", "oak" };
char **cursor;
printf("before: %s %s %s\n", words[0], words[1], words[2]);
swap_ptr(&words[0], &words[1]);
printf("after: %s %s %s\n", words[0], words[1], words[2]);
cursor = words;
printf("*cursor = %s, **cursor = %c\n", *cursor, **cursor);
cursor++;
printf("*cursor = %s, **cursor = %c\n", *cursor, **cursor);
return 0;
}
Example explained
Line 1swap_ptr(&words[0], &words[1]) passes the addresses of two array slots, so only two addresses are exchanged and the text stays where it was.
Line 2char *tmp = *a; reads one level down to copy a pointer value, which is why swap_ptr needs no knowledge of string lengths.
Line 3cursor = words; is legal because an array of char * converts to a pointer to its first element, and that element is itself a char *.
Line 4cursor++ advances by the size of one pointer, not one char, so **cursor jumps from the 'a' of "ash" to the 'b' of "birch".
Deleting a list node without a prev pointer
Holding the address of a link, rather than a copy of it, lets one loop remove the head and an interior node with identical code.
<stdio.h>
struct node { int v; struct node *next; };
static void delete_value(struct node **link, int v)
{
while (*link != NULL) {
if ((*link)->v == v) {
*link = (*link)->next;
return;
}
link = &(*link)->next;
}
}
int main(void)
{
struct node c = { 3, NULL };
struct node b = { 2, &c };
struct node a = { 1, &b };
struct node *head = &a;
struct node *p;
delete_value(&head, 1); /* unlinks the first node */
delete_value(&head, 3); /* unlinks the last node */
for (p = head; p != NULL; p = p->next)
printf("[%d]", p->v);
putchar('\n');
printf("head->v = %d\n", head->v);
return 0;
}
Example explained
Line 1struct node **link holds the address of head on the first iteration and the address of a next field afterwards, so both cases are the same case.
Line 2*link = (*link)->next; overwrites whichever link is currently held, which removes the need for a prev variable and for a special branch on the head.
Line 3link = &(*link)->next; takes the address of the next field instead of copying its value, keeping the loop one level above the list.
Line 4The first call rewrites main's head to point at b, proving that the caller's own variable was changed and not a copy.
Important notes
*pp++ and (*pp)++ are different: postfix ++ binds tighter than unary *, so the first advances pp itself while the second increments the pointer that pp points to.
Every extra level costs another load and another lifetime to manage; past two levels, a small struct or a typedef almost always reads better than int ***.
Common mistakes
Assigning to the parameter instead of through it: void grab(int *p) { p = malloc(sizeof *p); } changes only the local copy, so the caller's pointer is untouched and the block leaks as soon as the function returns.
Checking only the outer level: if (pp != NULL) printf("%s", *pp); still crashes when *pp is null or was never initialised, because each level of indirection is a separate object that needs its own guarantee.
Passing &grid to a char ** parameter when grid is char grid[3][6]: the type is char (*)[3][6], and forcing it through with a cast makes the callee read the first bytes of the text as if they were an address, which usually segfaults.
Try it yourself
Change, predict, then run
Write void trim_front(char **s) that advances *s past any leading spaces, then call it on char *msg = " hello"; and print msg to confirm the caller's own pointer moved rather than the characters.
Open the C workspaceCheck your understanding
Given int a = 1, b = 2; int *p = &a; int **pp = &p; and then the statement *pp = &b; what is the state afterwards?
- a becomes 2 and p still points to a
- a becomes 2 and p points to b
- a is still 1 and p now points to b
- The statement is undefined behaviour because pp holds the address of a pointer, not of an int
Show answer
*pp is another name for the object p, so the assignment stores a new address in p and no int is ever written; a keeps the value 1. The tempting answer that a becomes 2 describes **pp = b;, which peels both levels and lands on the int that p points at.