5.1.12 · D4C Programming

Exercises — Pointer to pointer

3,527 words16 min readBack to topic

Throughout we reuse this fixed memory snapshot so the numbers are always the same:

Figure — Pointer to pointer
Figure 1 — the fixed snapshot. Three rounded boxes in a row. Left (lavender) = 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
  • x holds the integer 42.
  • p holds &x = 0x100 (the address of x).
  • pp holds &p = 0x200 (the address of p).

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 of p).
  • *pp = p = 0x100 — follow one arrow: land on p, read its value (&x).
  • **pp = x = 42 — follow two arrows: through p to x, read 42.

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 to x).
  • pp = 0x200 (unchanged — still points to p).

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 3
Recall Solution L3.1
  • After line 1: *p names x, so x = 20. p still 0x100.
  • After line 2: **pp also names x, so x = 30. p still 0x100.
  • After line 3: *pp names p (one peel), so p = NULL (0x0). x unchanged 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
  • argc3 (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 — like char **.
  • B (char rows_b[2][3]): rows_b[1][0] = one address computation base + 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 2
Recall Solution L5.3
  • Line 1: *pp names p, so p = NULL. x still 42.
  • Line 2: **pp = *(*pp) = *(NULL)UB (undefined behaviour) / crash (writing through a NULL pointer). x is 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 **argv in L4.2.
  • Pointer arithmetic — the pp + 1 step in L3.3.
  • 2-D arrays vs pointer to pointer — L5.1 and L5.2 layout traps.