5.1.8 · D3C Programming

Worked examples — Pointers — declaration, dereferencing ( - ), address-of (&)

4,313 words20 min readBack to topic

The scenario matrix

Before any example, here is the full map of what "pointer problems" look like. The Cell column is just a label letter (A, B, C, …) so each worked example can announce which situation it covers — nothing mathematical, just an index tag. Each row is a case class: a distinct kind of situation.

Cell Case class The core question it tests Degenerate / edge twist
A Read-back (*(&x)) Do & and * really undo each other?
B Write-through (*p = v, where v is any value) Can a pointer change the original? writing through a copy vs original
C Function modifies caller Why pass an address, not a value? pass-by-value fails silently
D NULL / invalid pointer What happens at address 0 / garbage? the crash case
E Pointer + integer (stepping) Does p+1 mean +1 byte or +1 element? different types → different step
F Two pointers, same target (aliasing) What if two pointers point at one house? change via one, see via other
G sizeof — pointer vs target Which size does sizeof report? pointer size vs pointed-to size
H Declaration parsing (int *p, q;) Which names are actually pointers? the comma trap
I Word problem (real-world model) Can you translate a story into pointers?
J Exam twist (chained *, **) Pointer-to-pointer, double follow two hops of indirection
K Pointer subtraction & decrement What is p1 - p2? What does p-- do? distance-between and stepping back
L Dangling pointer after free Is a freed pointer safe to use? valid address that no longer owns memory

Twelve cells. Below are the worked examples, one (or more) per cell — most anchored to a memory picture so you see the arrows before you read the rule.


Picture first: the whole model on one board

Figure — Pointers — declaration, dereferencing ( - ), address-of (&)
Figure 1 — The street-of-houses model. Blue houses hold plain values; the yellow house p holds an address, shown as an arrow pointing at x. &x = "grab this house's number" (build the arrow). *p = "walk the arrow and look inside." This single picture underlies every case class in the matrix.


Example A — the inverse identity *(&x)

Steps.

  1. Evaluate &x. Why this step? Innermost first — &x builds the arrow to x's house, say 0x1000.
  2. Apply * to that address. Why this step? * walks the arrow: go to 0x1000, look inside → the int 5.
  3. So *(&x) = 5. Why this step? * walks the very arrow & just built — a round trip home, back to the value.

Verify: & builds an arrow, * walks it back. The pair is a no-op on the value: *(&x) == x == 5. ✓


Example B — writing through a pointer changes the original

Steps.

  1. int *p = &x; stores x's address inside p. Why this step? Now p's arrow points at the same house x occupies.
  2. *p = 99; says "walk the arrow, and write the value 99 at the far end." Why this step? * on the left of = is a destination, not a read. It targets the house p's arrow names.
  3. That house is x. So x becomes 99. Why this step? x is not a copy of the target — it is the target. x occupies one 4-byte range (sizeof(int) bytes), and p's arrow names the start of exactly that range: two names for one storage location.

Verify: printing x gives 99. Sanity check: if *p = 99 did not change x, pointers could never modify anything — contradicting their whole purpose. ✓

Figure — Pointers — declaration, dereferencing ( - ), address-of (&)
Figure 2 — Write-through. Before: x = 42, p arrows at x's 4-byte cell. *p = 99 follows the pink arrow and overwrites those bytes. After: reading x (same cell) shows 99. The arrow is why one variable can change "another."


Example C — swap: pass-by-value fails, pass-by-address works

Steps.

  1. bad(m,n) copies the values 3 and 7 into bad's private a,b. Why this step? C passes arguments by copy. bad swaps its own copies; main's m,n are untouched. → after (i): m=3, n=7 (unchanged).
  2. good(&m,&n) copies the addresses of m and n. Why this step? Now good holds arrows to main's houses, not copies of their contents.
  3. int t=*a; reads 3 from m's house; *a=*b; writes 7 into m's house; *b=t; writes 3 into n's house. Why this step? Every * walks an arrow back into main's memory, so the writes stick. → after (ii): m=7, n=3.

Verify: the only difference is &/*. Value-copy → no visible change (3,7). Address-copy → real swap (7,3). ✓

Figure — Pointers — declaration, dereferencing ( - ), address-of (&)
Figure 3 — Two rows tell the whole story. Top: bad swaps its own private copies (pink), so main's blue houses stay 3,7. Bottom: good holds arrows back to main's houses, so the *-writes land on the originals → 7,3. The &/* pair is the only difference.


Example D — the deadly dereference (NULL / invalid)

Steps.

  1. int *p = NULL; sets p to the standard "points at nothing" constant — an arrow deliberately pointing at no valid object (usually drawn as address 0, formally ((void*)0)). Why this step? NULL is a deliberate invalid value you can test against with == / !=.
  2. Line (i) *p = 5; tries to walk that arrow into forbidden ground. Why this step? No real object lives there → the OS traps the access → segmentation fault / crash (undefined behaviour).
  3. Line (ii) checks p != NULL first; since p is NULL, the if body is skipped. Why this step? The guard means the dangerous *p never executes. Nothing is written; no crash.

Verify: count the dereferences that actually run. Unguarded: 1 dereference of a NULL arrow → crash. Guarded: 0 dereferences → safe. Rule: never *p without knowing p points at valid memory.


Example E — pointer stepping depends on the type

Steps.

  1. pi+1 for an int *: C scales by sizeof(int)=4. Why this step? One "step" must land on the next int, not the next byte. So pi+1 = 0x1000 + 4 = 0x1004.
  2. *(pi+1) reads the int at 0x1004, which is a[1] = 20. Why this step? The type both sets the step and tells C to read 4 bytes as one int.
  3. pc+1 for a char *: C scales by sizeof(char)=1. Why this step? One char is one byte, so pc+1 = 0x2000 + 1 = 0x2001.
  4. *(pc+1) reads the char at 0x2001 = s[1] = 'B'. Why this step? Same offset index (1), different byte distance — because the type differs.

Verify: offsets in bytes = index × sizeof. int: 1×4=40x1004, value 20. char: 1×1=10x2001, value 'B'. Different byte-steps, same element index. ✓

Figure — Pointers — declaration, dereferencing ( - ), address-of (&)
Figure 4 — Why +1 is not +1 byte. Top: int cells are 4 bytes wide, so pi+1 jumps a full yellow-arrow width to 0x1004 (value 20). Bottom: char cells are 1 byte, so pc+1 jumps one narrow step to 0x2001 (value 'B'). Same index, different byte-distance — the type sets the stride.


Example F — aliasing: two pointers, one house

Steps.

  1. p and q both hold x's address. Why this step? Two arrows can point at the same house — this is called aliasing.
  2. *p = 20; writes 20 into that single house. Why this step? There is only one house; changing it through any arrow changes it for all arrows.
  3. Read x, *p, *q — all three look at the same house. Why this step? No copies exist; every path reaches the same byte-range.

Verify: prints 20 20 20. Sanity check: it is impossible for *p and *q to differ here, because both addresses are identical (&x). ✓


Example G — sizeof: pointer vs its target

Steps.

  1. sizeof(x) — size of an int. Why this step? x is an int, so 4.
  2. sizeof(p) — size of the pointer variable itself. Why this step? A pointer just stores an address; on 64-bit that address is 8 bytes — regardless of what it points to.
  3. sizeof(*p) — size of the thing pointed at. Why this step? *p has type int, so 4. This is the target's size, not the pointer's.

Verify: prints 4 8 4. Key contrast: sizeof(p)=8 (the address) vs sizeof(*p)=4 (the pointed-to int). All pointers on the machine are 8; only interpretation differs. ✓


Example H — the declaration comma trap

Steps.

  1. The * binds to the individual declarator, not the line. Why this step? Read it as "*p is an int" → p is a pointer-to-int. And "q is an int" → q is a plain int.
  2. So p is int *, q is int. Why this step? The comma starts a fresh declarator; the * did not carry over.
  3. To make both pointers: int *p, *q; (or one per line). Why this step? Each name needs its own *.

Verify: count the pointers: exactly 1 (p). q is a plain int. This is the classic misread. ✓


Example I — word problem: the shared scoreboard

Steps.

  1. Model the scoreboard as int score = 10;. Why this step? One real value, living in one house.
  2. Each assistant is a pointer to it: int *a1 = &score; int *a2 = &score;. Why this step? A "card with the location" is exactly &score — an address, not a copy.
  3. Assistant 1 adds 3: *a1 = *a1 + 3; → writes 13 to the scoreboard house. Why this step? Reading through a1 gives 10, add 3, write 13 back to the same house.
  4. Assistant 2 reads: *a2 → the same house → 13. Why this step? Both point at one scoreboard (aliasing again), so the change is visible to both.

Verify: assistant 2 reads 13. If they had held copies of the number, assistant 2 would still read 10 — that is precisely the difference pointers make. ✓


Example J — exam twist: pointer to a pointer (**)

Steps.

  1. p = &x — one hop: px. Why this step? Standard pointer: *p reaches x.
  2. pp = &ppp stores the address of p itself. Why this step? p is a variable too, so it has an address; pp is a pointer to a pointer.
  3. Read **pp step by step: *pp gives p (which holds &x); then a second * follows p to x. Why this step? Each * peels one layer: first hop pp→p, second hop p→x.
  4. **pp = 50; writes 50 at the end of the two hops — that endpoint is x. Why this step? Two levels of indirection still bottom out at x's house.

Verify: prints 50. Trace the hops: pp → p → x; writing through both arrows lands on x, so x = 50. ✓

Figure — Pointers — declaration, dereferencing ( - ), address-of (&)
Figure 5 — Two hops. pp arrows at p; p arrows at x. *pp walks the first arrow (lands on p); the second * walks p's arrow (lands on x). **pp = 50 therefore writes into x, so x becomes 50.


Example K — pointer subtraction and decrement

Steps.

  1. p1 - p2 on two int * into the same array gives the count of elements between them, not bytes. Why this step? C divides the raw byte-gap by sizeof(int). Byte-gap = 0x1010 - 0x1004 = 12 bytes; 12 / 4 = 3. So d = 3 (indices 4 - 1 = 3).
  2. Sanity-check the sign: p1 is ahead of p2 in the array, so p1 - p2 is positive (3). If we wrote p2 - p1 it would be -3. Why this step? Subtraction encodes direction: forward is +, backward is .
  3. p1-- moves p1 back one element, scaling by sizeof(int)=4. Why this step? Decrement mirrors increment: address drops by 4 bytes, from &a[4]=0x1010 to &a[3]=0x100C.
  4. Now *p1 reads a[3] = 40. Why this step? p1 now arrows at index 3; walking it reads 40.

Verify: d = (0x1010-0x1004)/4 = 3. After p1--: address 0x1010 - 4 = 0x100C = &a[3], so *p1 = 40. Both operations scale by sizeof(int) — exactly like p+1, just in reverse / as a difference. ✓

Figure — Pointers — declaration, dereferencing ( - ), address-of (&)
Figure 6 — Subtraction measures a gap; decrement steps back. Top: p1 - p2 counts elements between the two arrows (3), not bytes (12). Bottom: p1-- slides the arrow one 4-byte cell left, from a[4] to a[3], so *p1 becomes 40.


Example L — dangling pointer after free

Steps.

  1. malloc(sizeof(int)) hands back the address of a fresh heap cell; *p = 77 writes to it — all legal while we own it. Why this step? malloc gives us ownership of that house; *p on it is valid memory.
  2. free(p) returns the house to the system. Crucially, p still holds the old address — the arrow now points at a house we no longer own. Why this step? free frees the memory, not the pointer variable. The arrow is stale — this stale arrow is a dangling pointer.
  3. Line (i) *p = 5; walks that dangling arrow into a house that may be reused, corrupted, or handed to someone else → undefined behaviour (a subtle, often non-crashing bug — worse than a clean NULL crash). Why this step? Unlike NULL, a dangling pointer holds a plausible address, so the compiler and OS may not catch it — the damage can be silent.
  4. p = NULL; after free turns the dangling arrow into a known-invalid one you can test with if (p != NULL). Why this step? This converts a silent dangling bug (Cell L) back into the guardable NULL case (Cell D).

Verify: compare the two hazards. NULL-deref (Cell D): invalid and detectable (p == NULL). Dangling (Cell L): the address looks valid, so it slips past checks → undefined behaviour. The fix p = NULL moves you from the undetectable case to the detectable one. ✓


Active recall

Recall Symptom → which cell, and

why Predict each before revealing. Each answer says what cell and why the intuition holds. Function changed nothing after the call ::: Cell C — C copies arguments by value, so the function swapped private copies; pass &-addresses so its *-writes reach the originals. p+1 moved 4 bytes, not 1 ::: Cell E — pointer arithmetic scales by sizeof(*p) so one step lands on the next element, not the next byte. int *p, q; — is q a pointer? ::: Cell H — no; * binds to a single declarator, so only p is a pointer, q is a plain int. Program crashed at *p ::: Cell D — p was NULL/invalid; the arrow pointed at no valid object, so the OS trapped the access. Guard with if (p != NULL). sizeof(p) gave 8, not 4 ::: Cell G — that is the pointer's own size (an address is 8 bytes on 64-bit); sizeof(*p) would give the target's size (4). Used a pointer after free and got weird, non-crashing bugs ::: Cell L — dangling pointer; free released the memory but left the arrow's address intact, so it points at a house you no longer own → undefined behaviour. Set p = NULL right after free. p1 - p2 gave 3, not 12 ::: Cell K — pointer subtraction reports the gap in elements (bytes ÷ sizeof(int)), because that is the meaningful "distance" inside an array.

Recall Numeric answers to self-test (predict, then reveal)

Example B — x after *p = 99 ::: 99, because p's arrow and x name the same 4-byte cell. Example C — after value-call, then address-call ::: 3,7 (copies, no change), then 7,3 (arrows reach the originals). Example E — pi+1, *(pi+1), pc+1, *(pc+1) ::: 0x100420; 0x2001'B' (stride = sizeof(target)). Example F — the three printed numbers ::: 20 20 20, because both arrows point at one house. Example G — the three sizes ::: 4 8 4 (int, pointer, *p). Example I — assistant 2's reading ::: 13, because both assistants share the scoreboard's location. Example J — printed x after **pp = 50 ::: 50, because two hops bottom out at x. Example K — d and *p1 after p1-- ::: d = 3 (elements apart), *p1 = 40 (now at a[3]).


Connections

  • Parent: Pointers basics — the machinery these examples exercise.
  • Pass by Reference vs Pass by Value — Cell C in depth.
  • Pointer Arithmetic — Cells E and K: why steps scale by sizeof(*p), and subtraction/decrement.
  • Arrays in C — Cells E, K: array-to-pointer decay.
  • NULL and Segmentation Faults — Cells D, L: invalid and dangling dereferences.
  • Dynamic Memory malloc free — Cell L: pointers to heap memory and dangling after free.
  • Strings as char pointers — Cell E with char *.

Case-class map

Pointer scenarios

A read-back

B write-through

C function modifies caller

D NULL or invalid

E stepping by type

F aliasing two names

G sizeof pointer vs target

H declaration comma trap

I word problem

J double indirection

K subtraction and decrement

L dangling after free