Exercises — Pointer to pointer
Throughout we reuse this fixed memory snapshot so the numbers are always the same:

pp, living at address 0x300, holding the value 0x200. Middle (coral) = p, living at 0x200, holding 0x100. Right (mint/green) = x, living at 0x100, holding the integer 42. A lavender arrow runs from pp to p (because pp holds &p); a coral arrow runs from p to x (because p holds &x). The butter strip beneath restates: *pp = p = 0x100 and **pp = x = 42. The italic text under each box is its home address; the number inside each box is the value it stores.
Level 1 — Recognition
Goal: read the declaration and the arrows, no computation.
L1.1
Problem. For the snapshot above, fill in the value stored inside each box: what does x hold, what does p hold, what does pp hold?
Recall Solution L1.1
xholds the integer42.pholds&x = 0x100(the address ofx).ppholds&p = 0x200(the address ofp).
Why. A variable's box stores its value. For a pointer, that value happens to be an address. pp is a pointer to a pointer, so the value in its box is the address of another pointer (p). Since p lives at 0x200, pp holds 0x200.
L1.2
Problem. Which of these declarations is a pointer to a pointer to a char? Pick one.
(a) char *c;
(b) char **c;
(c) char c[];
(d) char ***c;Recall Solution L1.2
Answer: (b) char **c.
Why. Count the asterisks. One * = pointer to char. Two * = pointer to (pointer to char) = pointer to a pointer. (d) has three *, one level too many. (a) is a single pointer, (c) is an array.
Level 2 — Application
Goal: dereference the chain and produce concrete values.
L2.1
Problem. Using the snapshot, give the value of each expression:
pp, *pp, **pp.
Recall Solution L2.1
pp = 0x200— the address it holds (address ofp).*pp = p = 0x100— follow one arrow: land onp, read its value (&x).**pp = x = 42— follow two arrows: throughptox, read42.
Why. Each * "peels a layer" — follow the address to the thing it points at. Two * peels twice, landing on the integer.
L2.2
Problem. Fill the table (snapshot values). Give both the type and the value.
| Expression | Type | Value |
|---|---|---|
&pp |
? | ? |
*pp |
? | ? |
**pp |
? | ? |
Recall Solution L2.2
| Expression | Type | Value |
|---|---|---|
&pp |
int *** |
0x300 (the home address of pp from the snapshot) |
*pp |
int * |
0x100 |
**pp |
int |
42 |
Why. & wraps (adds a level, gives an address): &pp is the address of pp. From the snapshot pp lives at 0x300, so &pp = 0x300, and its type gains a star → int ***. * peels (removes a level): *pp is int * (0x100), **pp is int (42). Star peels, ampersand wraps.
L2.3
Problem. After the snapshot we run **pp = 7;. What is x now? What is p? What is pp?
Recall Solution L2.3
x = 7(changed).p = 0x100(unchanged — still points tox).pp = 0x200(unchanged — still points top).
Why. The left side **pp peels two layers, so it names the integer x. We wrote to that innermost box only. The pointers along the way were used to navigate, not overwritten.
Level 3 — Analysis
Goal: reason about which level a write hits, and about broken chains.
L3.1
Problem. Starting fresh from the snapshot, execute in order and give x, p after each line:
*p = 20; // line 1
**pp = 30; // line 2
*pp = NULL; // line 3Recall Solution L3.1
- After line 1:
*pnamesx, sox = 20.pstill0x100. - After line 2:
**ppalso namesx, sox = 30.pstill0x100. - After line 3:
*ppnamesp(one peel), sop = NULL (0x0).xunchanged at 30.
Why. Lines 1 and 2 reach x by different roads but the same destination — both write the integer. Line 3 uses one star, landing on the pointer p and overwriting it. The chain pp → p → x is now broken at p.
L3.2
Problem. Continuing right after L3.1 (so p == NULL), a student writes printf("%d", **pp);. What happens and why?
Recall Solution L3.2
Undefined behaviour (UB) — almost certainly a crash.
Why. **pp = *(*pp) = *(p). But p is now NULL. Dereferencing NULL reads memory that isn't yours → segmentation fault (typical). The first peel (*pp = p = NULL) is fine; the second peel dereferences that NULL. Each link in a ** chain must point somewhere valid.
L3.3
Problem. pp holds 0x200. Explain what pp + 1 points to and why. Assume a pointer is 8 bytes.
Recall Solution L3.3
pp has type int **, i.e. it points to objects of type int *. See Pointer arithmetic: pp + 1 advances by sizeof(int *) = 8 bytes, giving 0x200 + 8 = 0x208.
Why. Pointer arithmetic steps in units of the pointed-to type's size, not bytes. pp points to int * objects (8 bytes each), so +1 moves 8 bytes forward — to where the next pointer would sit if p were part of an array of pointers.
Edge-case caveat (important). The C standard only defines pointer arithmetic within a single array object, plus one slot past its end (the "one-past-the-end" address, which you may form but must not dereference). Here p is a lone variable, not an array element, so forming pp + 1 and especially dereferencing it is formally undefined behaviour (UB) — even though the address computation 0x208 is what a typical machine would produce. The arithmetic is only legitimate when pp genuinely points into an array of int * (e.g. char **argv, or a malloc'd block of pointers).
Level 4 — Synthesis
Goal: write small correct programs that need **.
L4.1
Problem. Complete this function so that after alloc_int(&q) the caller's q points to fresh memory holding 100. Fill the two blanks, and guard against allocation failure.
#include <stdio.h> // printf, NULL
#include <stdlib.h> // malloc, NULL
void alloc_int(int **ptr) {
____ = malloc(sizeof(int)); // blank A
____ = 100; // blank B
}
int main(void) {
int *q = NULL;
alloc_int(&q);
printf("%d\n", *q); // must print 100
}Recall Solution L4.1
#include <stdio.h> // printf, NULL
#include <stdlib.h> // malloc, NULL
void alloc_int(int **ptr) {
*ptr = malloc(sizeof(int)); // blank A: *ptr IS the caller's q
if (*ptr == NULL) return; // malloc can fail → bail out, leave q == NULL
**ptr = 100; // blank B: safe now, write into the new memory
}Output on success: 100.
Why the includes? malloc is declared in <stdlib.h>; printf in <stdio.h>. The macro NULL is defined in <stddef.h> and is also pulled in by <stdio.h> and <stdlib.h> — so including either of those is enough for NULL here. Reproduce the example without these #include lines and the compiler will complain about undeclared malloc/printf or an unknown NULL.
Why the double star? See Pass by value vs pass by reference: C copies arguments, so to change the caller's pointer q we pass its address (&q, type int **). Inside, *ptr names q itself → assigning the malloc result to *ptr updates q.
Why the NULL check? See Dynamic memory allocation (malloc): malloc returns NULL when memory cannot be allocated. If we skip the check and memory ran out, *ptr is NULL, so **ptr = 100 dereferences NULL → undefined behaviour (UB) / crash. Robust pointer code always checks the malloc result before the second peel writes through it.
L4.2
Problem. A program runs as ./prog cat dog. Using int main(int argc, char **argv), what do these print?
printf("%d\n", argc);
printf("%s\n", argv[0]);
printf("%s\n", *(argv + 1));
printf("%c\n", **(argv + 2));Recall Solution L4.2
argc→3(program name + two args).argv[0]→"./prog"(the program name).*(argv + 1)→"cat"(the string pointer at index 1).**(argv + 2)→'d'(first char of"dog").
Why. See Command line arguments (argv): argv is char ** — a pointer to an array of char * strings. argv + 2 steps to the third slot (by sizeof(char *)); one * gives the string pointer "dog"; a second * peels to its first character 'd'. (Note this arithmetic is well-defined: argv genuinely points into an array of char *.)
L4.3
Problem. Write a function void swap_ptrs(int **a, int **b) that swaps which integers two pointers point to, then show what p1/p2 point to after calling it.
int u = 1, v = 2;
int *p1 = &u, *p2 = &v;
swap_ptrs(&p1, &p2);Recall Solution L4.3
void swap_ptrs(int **a, int **b) {
int *tmp = *a; // save p1's target
*a = *b; // p1 now points where p2 did
*b = tmp; // p2 now points where p1 did
}After the call: p1 points to v (so *p1 == 2), p2 points to u (so *p2 == 1).
Why. We want to change the pointers themselves, so we pass their addresses (int **). Inside, *a is the caller's p1; assigning to *a retargets p1. u and v are never moved — only which arrow points where changes.
Level 5 — Mastery
Goal: subtle undefined behaviour (UB), memory layout, and multi-level reasoning.
L5.1
Problem. Is this valid? Explain precisely.
int arr[3] = {5, 6, 7};
int **pp = &arr; // <-- ?Recall Solution L5.1
Invalid — type mismatch. In most expressions arr decays to int * (a temporary pointer to the first element). But here arr is the operand of unary &, and array-to-pointer decay does not happen in the operand of & — one of the few exceptions to the decay rule. So &arr keeps the array type intact and has type int (*)[3] (pointer to array of 3 ints), not int **.
Why it can't be int **. See 2-D arrays vs pointer to pointer. A real int ** must point to an actual int * object that lives somewhere in memory. An array's name is not a stored pointer — it's an address computed on the fly — so there is no pointer object to take a two-level address of. The correct two-level setup needs an intermediate pointer variable: int *ip = arr; int **pp = &ip; (now ip is a genuine int * that lives in memory, and &ip is a real int **).
L5.2
Problem. Compare the memory layouts. Which access is a genuine double dereference, and which is a single scaled index?
// A: a "jagged" array of pointers
char *rows_a[2] = {"hi", "yo"}; // char *[2]
// B: a real 2-D array
char rows_b[2][3] = {"hi", "yo"}; // char [2][3]For each, what does accessing element [1][0] cost in "follow-a-pointer" steps?
Recall Solution L5.2
- A (
char *rows_a[2]):rows_a[1][0]=*(*(rows_a + 1) + 0). Two dereferences: first follow the pointer stored in slot 1 (to the string"yo"), then read its first char'y'. This is the genuine double-indirection model — likechar **. - B (
char rows_b[2][3]):rows_b[1][0]= one address computationbase + 1*3 + 0, then a single read. No stored pointers exist; the address is computed by multiplying, not by following an arrow.
Both yield 'y', but A follows a pointer while B computes an offset. That is the core 2-D array vs pointer to pointer distinction. See 2-D arrays vs pointer to pointer.
Why. In A, rows_a is an array of two char * values; those pointers really sit in memory. To reach a character you must read a stored pointer first (peel one), then read a char through it (peel two) — genuine double indirection. In B, rows_b is 2*3 = 6 chars laid out contiguously with no pointers anywhere; [1][0] is turned by the compiler into the single offset 1*3 + 0 = 3 from the base, so exactly one memory read happens. The [i][j] syntax looks identical, but A costs two follows while B costs one computed index — that is why the two types are not interchangeable.
L5.3
Problem. Trace this and state the final x, plus whether any line is UB.
int x = 42;
int *p = &x;
int **pp = &p;
*pp = NULL; // line 1
**pp = 5; // line 2Recall Solution L5.3
- Line 1:
*ppnamesp, sop = NULL.xstill42. - Line 2:
**pp = *(*pp) = *(NULL)→ UB (undefined behaviour) / crash (writing through aNULLpointer).xis never reached.
Final: x remains 42 (if it survived); the program has undefined behaviour at line 2.
Why. Line 1 broke the chain at p. Line 2 must peel through p to reach x, but p is now NULL, so the second peel dereferences NULL. Order matters: a valid-looking **pp becomes deadly once an inner link is nulled.
Recall Grade yourself
- Got L1–L2 clean → you can read and dereference chains.
- Got L3 → you understand which level a write hits and broken chains.
- Got L4 → you can write
**code (functions,argv, swaps). - Got L5 → you grasp UB ordering and the array-vs-
**layout gap. Mastery.
Connections
- Parent — Pointer to pointer
- Pointers basics — the single-level foundation.
- Pass by value vs pass by reference — why
**for functions (L4.1, L4.3). - Dynamic memory allocation (malloc) — used in L4.1.
- Command line arguments (argv) —
char **argvin L4.2. - Pointer arithmetic — the
pp + 1step in L3.3. - 2-D arrays vs pointer to pointer — L5.1 and L5.2 layout traps.