Worked examples — Pointers — declaration, dereferencing ( - ), address-of (&)
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

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.
- Evaluate
&x. Why this step? Innermost first —&xbuilds the arrow tox's house, say0x1000. - Apply
*to that address. Why this step?*walks the arrow: go to0x1000, look inside → theint5. - 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.
int *p = &x;storesx's address insidep. Why this step? Nowp's arrow points at the same housexoccupies.*p = 99;says "walk the arrow, and write the value99at the far end." Why this step?*on the left of=is a destination, not a read. It targets the housep's arrow names.- That house is
x. Soxbecomes99. Why this step?xis not a copy of the target — it is the target.xoccupies one 4-byte range (sizeof(int)bytes), andp's arrow names the start of exactly that range: two names for one storage location.
Verify: printing
xgives99. Sanity check: if*p = 99did not changex, pointers could never modify anything — contradicting their whole purpose. ✓

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.
bad(m,n)copies the values3and7intobad's privatea,b. Why this step? C passes arguments by copy.badswaps its own copies;main'sm,nare untouched. → after (i):m=3, n=7(unchanged).good(&m,&n)copies the addresses ofmandn. Why this step? Nowgoodholds arrows tomain's houses, not copies of their contents.int t=*a;reads3fromm's house;*a=*b;writes7intom's house;*b=t;writes3inton's house. Why this step? Every*walks an arrow back intomain'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). ✓

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.
int *p = NULL;setspto the standard "points at nothing" constant — an arrow deliberately pointing at no valid object (usually drawn as address0, formally((void*)0)). Why this step?NULLis a deliberate invalid value you can test against with==/!=.- 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). - Line (ii) checks
p != NULLfirst; sincepis NULL, theifbody is skipped. Why this step? The guard means the dangerous*pnever 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
*pwithout knowingppoints at valid memory. ✓
Example E — pointer stepping depends on the type
Steps.
pi+1for anint *: C scales bysizeof(int)=4. Why this step? One "step" must land on the nextint, not the next byte. Sopi+1=0x1000 + 4 = 0x1004.*(pi+1)reads theintat0x1004, which isa[1] = 20. Why this step? The type both sets the step and tells C to read 4 bytes as oneint.pc+1for achar *: C scales bysizeof(char)=1. Why this step? One char is one byte, sopc+1=0x2000 + 1 = 0x2001.*(pc+1)reads thecharat0x2001=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=4→0x1004, value20. char:1×1=1→0x2001, value'B'. Different byte-steps, same element index. ✓

+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.
pandqboth holdx's address. Why this step? Two arrows can point at the same house — this is called aliasing.*p = 20;writes20into that single house. Why this step? There is only one house; changing it through any arrow changes it for all arrows.- 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*pand*qto differ here, because both addresses are identical (&x). ✓
Example G — sizeof: pointer vs its target
Steps.
sizeof(x)— size of anint. Why this step?xis anint, so4.sizeof(p)— size of the pointer variable itself. Why this step? A pointer just stores an address; on 64-bit that address is8bytes — regardless of what it points to.sizeof(*p)— size of the thing pointed at. Why this step?*phas typeint, so4. This is the target's size, not the pointer's.
Verify: prints
4 8 4. Key contrast:sizeof(p)=8(the address) vssizeof(*p)=4(the pointed-toint). All pointers on the machine are8; only interpretation differs. ✓
Example H — the declaration comma trap
Steps.
- The
*binds to the individual declarator, not the line. Why this step? Read it as "*pis anint" →pis a pointer-to-int. And "qis anint" →qis a plain int. - So
pisint *,qisint. Why this step? The comma starts a fresh declarator; the*did not carry over. - 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).qis a plain int. This is the classic misread. ✓
Example I — word problem: the shared scoreboard
Steps.
- Model the scoreboard as
int score = 10;. Why this step? One real value, living in one house. - 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. - Assistant 1 adds 3:
*a1 = *a1 + 3;→ writes13to the scoreboard house. Why this step? Reading througha1gives10, add3, write13back to the same house. - 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 read10— that is precisely the difference pointers make. ✓
Example J — exam twist: pointer to a pointer (**)
Steps.
p = &x— one hop:p→x. Why this step? Standard pointer:*preachesx.pp = &p—ppstores the address ofpitself. Why this step?pis a variable too, so it has an address;ppis a pointer to a pointer.- Read
**ppstep by step:*ppgivesp(which holds&x); then a second*followsptox. Why this step? Each*peels one layer: first hoppp→p, second hopp→x. **pp = 50;writes50at the end of the two hops — that endpoint isx. Why this step? Two levels of indirection still bottom out atx's house.
Verify: prints
50. Trace the hops:pp → p → x; writing through both arrows lands onx, sox = 50. ✓

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.
p1 - p2on twoint *into the same array gives the count of elements between them, not bytes. Why this step? C divides the raw byte-gap bysizeof(int). Byte-gap =0x1010 - 0x1004 = 12bytes;12 / 4 = 3. Sod = 3(indices4 - 1 = 3).- Sanity-check the sign:
p1is ahead ofp2in the array, sop1 - p2is positive (3). If we wrotep2 - p1it would be-3. Why this step? Subtraction encodes direction: forward is+, backward is−. p1--movesp1back one element, scaling bysizeof(int)=4. Why this step? Decrement mirrors increment: address drops by 4 bytes, from&a[4]=0x1010to&a[3]=0x100C.- Now
*p1readsa[3] = 40. Why this step?p1now arrows at index 3; walking it reads40.
Verify:
d = (0x1010-0x1004)/4 = 3. Afterp1--: address0x1010 - 4 = 0x100C = &a[3], so*p1 = 40. Both operations scale bysizeof(int)— exactly likep+1, just in reverse / as a difference. ✓

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.
malloc(sizeof(int))hands back the address of a fresh heap cell;*p = 77writes to it — all legal while we own it. Why this step?mallocgives us ownership of that house;*pon it is valid memory.free(p)returns the house to the system. Crucially,pstill holds the old address — the arrow now points at a house we no longer own. Why this step?freefrees the memory, not the pointer variable. The arrow is stale — this stale arrow is a dangling pointer.- 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. p = NULL;afterfreeturns the dangling arrow into a known-invalid one you can test withif (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 fixp = NULLmoves 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) ::: 0x1004→20; 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 *.