C Part I · Module 7 · Pointers
Pointers to Pointers
A pointer is an object, and a pointer identifies an object, so nothing stops a pointer from identifying another pointer. That observation is easy and is where most explanations of this topic stop, leaving the reader with a notation and no reason to use it — which is why double pointers have a reputation for being gratuitously confusing. They are not confusing when they are being used for something. This chapter covers the two situations that genuinely require a second level of indirection, both of which you have already met as unfinished business: a function that needs to change which object its caller's pointer identifies, and what happens when the array of pointers from the previous chapter is passed to a function. Neither needs any machinery beyond what Module 7 has already built.
Advanced17 min readCPart IFundamentalsPointers
Part I · Module 7 · Chapter 7.10 · Pointers to Pointers
1. The Type
int value = 42;
int *p = &value;
int **pp = &p;Read each declaration with Chapter 7.9 §3's technique — start at the name, go right while you can, then left:
int *p p is a pointer to int
int **pp pp is a pointer to a pointer to intpp is an ordinary object. It holds an address, like every pointer. What is at that address happens to be another pointer.
Dereferencing works one level at a time:
pp an int ** — the address of p
*pp an int * — p itself
**pp an int — valueThere is no new rule here. * removes one level, exactly as it always has.
2. Why You Should Be Suspicious of It
Before the uses, the caution — because this is a construct that gets reached for far more often than it is justified.
Every level of indirection is something a reader has to hold in their head, and something that can be null, stale or wrong. A second level needs a reason.
In ordinary C, two levels is the practical ceiling. Three is almost always a sign that something should have been a different shape. The two sections that follow are the reasons that do justify a second level, and they are specific.
3. Problem One: Changing the Caller's Pointer
Chapter 7.5 §4 established that assigning to a pointer parameter changes nothing in the caller:
void attempt_retarget(int *p) { int local = 99; p = &local; }and said, at the time, that if you need the caller to see the change you want a pointer to a pointer. Here is that situation for real.
A function is to search an array and tell the caller which element it chose — not the value, the element, so the caller can modify it or compute its index. The answer is a pointer, and the caller has a pointer object waiting for it.
#include <stdio.h>
#include <stddef.h>
void select_largest(int *values, size_t count, int **chosen_out)
{
if (chosen_out == NULL || count == 0u) {
return;
}
int *best = &values[0];
for (size_t i = 1; i < count; i++) {
if (values[i] > *best) {
best = &values[i];
}
}
*chosen_out = best;
}
void select_largest_broken(int *values, size_t count, int *chosen)
{
(void)count;
chosen = &values[0];
}
int main(void)
{
int readings[5] = {12, 45, 7, 45, 31};
size_t count = sizeof readings / sizeof readings[0];
int *chosen = NULL;
select_largest(readings, count, &chosen);
printf("chosen != NULL %d\n", chosen != NULL);
printf("*chosen %d\n", *chosen);
printf("index of chosen %td\n", chosen - readings);
int *broken = NULL;
select_largest_broken(readings, count, broken);
printf("broken still NULL %d\n", broken == NULL);
return 0;
}Output:
chosen != NULL 1
*chosen 45
index of chosen 1
broken still NULL 1The last line is Chapter 7.5 §4 restated: select_largest_broken assigned to its own parameter, and the caller's pointer is untouched. The compiler even notices that the assignment is pointless:
warning: parameter 'chosen' set but not used [-Wunused-but-set-parameter]select_largest works because the caller passed &chosen — the address of its own pointer object — and the function wrote through it with *chosen_out = best;.
This is Chapter 7.5's out-parameter pattern with one difference: the thing being written back is itself a pointer, so the out-parameter needs one more level.
To let a function change a caller's
T, pass aT *. To let it change a caller'sT *, pass aT **. It is the same rule, applied to a type that happens to be a pointer.
Three details of select_largest are worth copying.
The null guard covers the out-parameter, as Chapter 7.4 §5 requires for any pointer arriving from a caller — and here the pointer being guarded is the int **, not what it points at.
count == 0 is handled before values[0] is read, which is Chapter 6.3 §7's warning about the find-the-extreme pattern needing at least one element.
The caller initialises chosen to NULL and could check it afterwards. If the function returns without choosing anything, the caller can tell — which is Chapter 7.4 §5's deliberate use of NULL.
4. Problem Two: Passing an Array of Pointers
This one requires no new idea at all — it is Chapter 7.6 and Chapter 7.9 composed.
Chapter 7.9 built int *counters[4], an array whose elements are int *. Chapter 7.6 established that an array in an expression converts to a pointer to its first element. The first element of an array of int * is an int *, so a pointer to it is an int **.
#include <stdio.h>
#include <stddef.h>
int total_of(int *table[], size_t n)
{
int total = 0;
for (size_t i = 0; i < n; i++) {
total = total + *table[i];
}
return total;
}
int main(void)
{
int a = 1, b = 2, c = 3;
int *t[3] = { &a, &b, &c };
printf("sizeof t in main %zu\n", sizeof t);
printf("total_of(t, 3) %d\n", total_of(t, 3));
return 0;
}Output:
sizeof t in main 24
total_of(t, 3) 6int *table[] in that parameter list is, by Chapter 7.7's adjustment rule, int **table. The two spellings are identical, and you will see both.
The count still has to be passed separately, for exactly Chapter 7.7's reason — sizeof t is 24 in main and would be a pointer's size inside the function.
table[i] is one pointer; *table[i] is the int it identifies. The same two steps as Chapter 7.9 §2, now one function boundary away.
This is where most working C programmers meet
T **for the first time, and it is worth recognising it as an array of pointers that has decayed rather than as a mysterious new type.
5. Reading Deeper Declarations
int x an int
int *p a pointer to int
int **pp a pointer to a pointer to int
int ***ppp a pointer to a pointer to a pointer to intThe notation extends indefinitely, and the reading technique from Chapter 7.9 §3 keeps working. That does not make the deeper forms a good idea.
6. The Shape You Will Meet Next
Both problems in this chapter had a second level because something genuinely lived at each one. There is a third situation, which this curriculum has not reached: a function that obtains storage and hands it back to the caller through a pointer parameter. That is Chapter 9.6's, and it is Problem One with the object being newly acquired rather than selected from an array.
Mentioning it now is useful because it is the case most code uses, and because it is a direct instance of §3 rather than a new pattern — when you meet it, the only new thing will be where the pointer came from.
7. What You Should Be Able to Say Now
int **ppis a pointer to a pointer toint— an ordinary object holding an address.*removes one level at a time:pp→*pp→**pp.- Every level needs a reason; two is the practical ceiling in ordinary code.
- Problem one: to let a function change a caller's pointer, pass the pointer's address.
- That is Chapter 7.5's out-parameter with the result type being a pointer — same rule, one more level.
- The broken version assigns to the parameter and the caller's pointer stays
NULL— Chapter 7.5 §4, demonstrated. - Problem two: an array of pointers decays to
T **when passed — Chapter 7.6 and 7.9 composed. int *table[]andint **tableare the same parameter, by Chapter 7.7's adjustment.- The count still travels separately, for Chapter 7.7's reason.
- Three or more levels is a design smell — usually the data wants a structure instead.
8. Exercises
Exercise 1 — read the declarations. Say in words what each declares.
char **a;
double *b[4];
int **c[2];
int (*d)[3];Exercise 2 — peel the levels. Given int v = 7; int *p = &v; int **pp = &p;, give the value or meaning of each.
v p *p pp *pp **pp &ppExercise 3 — predict the output. Do not compile yet.
int a = 1, b = 2;
int *p = &a;
int **pp = &p;
printf("%d\n", **pp);
*pp = &b;
printf("%d %d\n", **pp, *p);
**pp = 20;
printf("%d %d\n", a, b);The second line is the one to reason about — say what *pp = &b changed.
Exercise 4 — fix the signature. This is meant to leave the caller's pointer aimed at the first element. Say why it does not, give the corrected signature and body, and give the corrected call.
void aim_at_start(int *values, int *out) { out = &values[0]; }
...
int *where = NULL;
aim_at_start(data, where);Exercise 5 — identify the decay. For double *table[6]; passed to a function, say what the parameter's type really is, give two spellings of it, and say what sizeof of that parameter would be.
Exercise 6 — justify or refuse. For each, say whether a second level of indirection is justified, and if not, what you would do instead.
- a function that must set the caller's pointer to the chosen element
- a function that must set the caller's
intto a computed value - a traversal over an array of pointers passed from elsewhere
- a function that returns one of three pointers it was given
Exercise 7 — count the null checks. In §3's select_largest, say which pointers could be null, which ones the function checks, and why the ones it does not check do not need checking.
Exercise 8 — argue the ceiling. A colleague proposes int ***data for a structure with three levels of grouping. Give two concrete reasons to resist, and name the Module that provides the better tool.
9. Summary
int **ppis an ordinary object whose value is the address of anint *.*removes one level at a time, with no new rule.- Every level must earn its place; two is the practical ceiling.
- Problem one — change the caller's pointer: pass its address, write
*out = .... - Assigning to the parameter instead leaves the caller's pointer unchanged, as verified.
- Problem two — an array of pointers decays to
T **;int *t[]andint **tare the same parameter. - The count still travels separately.
- Three or more levels usually means the data wants a structure — Module 10.
- A function that acquires storage and hands it back is Problem One again, in Chapter 9.6.
Several signatures in this module have carried a qualifier that has been used without being explained. Chapter 7.7's sum took a const int *, and the text said only that it meant the function would not write through the pointer. That was accurate and incomplete, because const can attach to two different things in a pointer declaration — the object being pointed at, or the pointer itself — and the two placements look nearly identical and forbid completely different operations. The next chapter reads them properly.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
