Worked examples — Function pointers — declaration, calling, use in callbacks
This page is the "run every case" workshop for Function pointers. The parent note built the ideas; here we exercise them until no scenario can surprise you. If a symbol shows up you don't recognise, it was defined in the parent — but we'll re-anchor the important ones as we go.
Prerequisites we lean on: Pointers in C, Arrays in C, void pointers, typedef, qsort and the C Standard Library, and the idea of Polymorphism.
The scenario matrix
Before working examples, let's list every kind of situation a function pointer can put in front of you. Think of this like the "all quadrants + zero + limits" checklist for angles — but for code. Each row is a case class; every example below is tagged with the cell it fills.
| # | Case class | What makes it tricky | Example that covers it |
|---|---|---|---|
| A | Declare + call a plain fn pointer | (*p) parentheses; two call syntaxes |
Ex 1 |
| B | Ascending sort via qsort (positive sign) |
comparator returns positive | Ex 2 |
| C | Descending sort (flipped sign) | comparator returns negative | Ex 2 |
| D | Equal elements (zero case) | comparator returns 0, stability | Ex 3 |
| E | Degenerate input: array of size 0 or 1 | callback may never fire | Ex 4 |
| F | Dispatch table (array of fn pointers) | index selects behaviour | Ex 5 |
| G1 | Overflow trap (a-b) |
subtraction returns wrong sign | Ex 6 |
| G2 | Signature mismatch (wrong type) | UB from calling through mismatched type | Ex 7 |
| H | NULL / uninitialised pointer (degenerate) | calling garbage → crash | Ex 8 |
| I | Real-world word problem | callback as "behaviour parameter" | Ex 9 |
| J | Exam-style twist (pointer to fn returning pointer) | reading gnarly declarations | Ex 10 |
Ex 1 — Declare, assign, call (Cell A)
Ex 2 — Ascending and descending sort (Cells B & C)
The figure below shows the single rule that drives every sort: qsort reads only the sign of your comparator. Three coloured buckets — teal for negative (first goes before second), plum for zero (equal), burnt orange for positive (first goes after second) — and an orange arrow tracing the worked case x=3, y=1 → +1. Keep this picture in mind whenever a comparator returns a number.

Ex 3 — Equal elements: the zero case (Cell D)
Ex 4 — Degenerate inputs: size 0 and size 1 (Cell E)
int a1[] = {42}; // size 1 (valid C)
int a3[] = {7, 8, 9}; // we'll pass only the first 0 of these
qsort(a3, 0, sizeof(int), cmp_asc); // nmemb = 0: sort "nothing"
qsort(a1, 1, sizeof(int), cmp_asc); // nmemb = 1(Note: standard C has no zero-length array literal like int a0[] = {};, so we model "size 0" by passing nmemb = 0 on a real array — qsort simply treats it as "sort the first 0 elements.")
How many times is cmp_asc called in each case, and what are the arrays afterward?
Forecast: Guess the number of comparator calls for nmemb = 0 and nmemb = 1.
- Count comparisons needed. To decide an order you must compare at least two elements. With
nmemb = 0ornmemb = 1there is no pair. Why this step? No pair → nothing to compare → the callback fires 0 times. - Result stays put. Nothing to reorder: with
nmemb=0a3is untouched ({7,8,9}), anda1stays{42}. Why this step? A sort of ≤1 element is already sorted — the identity case, like the limiting value in a formula.
Verify: comparator calls = 0 for both; a1 = {42}; a3 unchanged. Sanity: a 1-element list is trivially sorted; a well-written comparator never being called means no NULL-deref risk here either. ✓
Recall Why is this the "limiting value" of sorting?
Just as an angle formula must behave at , sorting must behave at the empty and singleton lists — the boundary where the general algorithm collapses to "do nothing." ::: Correct: nmemb ≤ 1 → 0 comparisons → array unchanged.
Ex 5 — Dispatch table (Cell F)
The figure below draws the dispatch table as three top slots (ops[0], ops[1], ops[2]), each holding a code address, with a coloured arrow pointing down to the actual function body (add → a+b in teal, sub → a-b in orange, mul → a*b in plum). The caption traces ops[1](10,4) → sub(10,4) → 6. Read it as "index picks a wire, the wire runs code."

int add(int a,int b){return a+b;}
int sub(int a,int b){return a-b;}
int mul(int a,int b){return a*b;}
int (*ops[3])(int,int) = { add, sub, mul };Evaluate ops[0](10,4), ops[1](10,4), ops[2](10,4), and (*ops[2])(10,4).
Forecast: Predict all four numbers.
- Read the array type.
int (*ops[3])(int,int)= "array of 3 pointers toint(int,int)." The[3]binds after(*...), so it's an array of pointers, not a pointer to an array. Why this step? Mis-reading this is the classic exam trap; the figure shows three slots each holding a code address. - Index selects a function.
ops[0]→add,ops[1]→sub,ops[2]→mul. Why this step? The index is the opcode — O(1) dispatch replaces anif/elsechain. - Call each:
add(10,4)=14,sub(10,4)=6,mul(10,4)=40. Why this step? Each slot carries different behaviour; only the index changed. - Explicit deref of a slot.
(*ops[2])(10,4)=mul(10,4) = 40— identical toops[2](10,4). Why this step? Confirms the deref-equivalence rule from Ex 1 still holds inside arrays.
Verify: 14, 6, 40, 40. Sanity: 10+4=14, 10-4=6, 10*4=40. ✓ This is exactly how bytecode interpreters route opcodes.
Ex 6 — Overflow trap: why a - b is a landmine (Cell G1)
A student writes return a - b; inside a comparator and tests with:
int a = 2000000000; // ~2e9
int b = -2000000000; // ~-2e9With 32-bit int (max ≈ 2147483647), what does the mathematical value a - b want to be, and why is computing it dangerous?
Forecast: Guess whether a - b gives a correct positive result.
- True mathematical value. .
Why this step? We need the ideal answer to compare against what a 32-bit
intcan actually hold. - Compare to the
intceiling. , so the true value cannot fit in a signed 32-bitint. Why this step? In standard C, computing a signed result that exceedsINT_MAXis undefined behaviour — not a guaranteed wrap. The program may produce a garbage value, a wrong sign, or trap; the standard makes no promise. That unpredictability is exactly why the comparator is broken. - Concrete symptom. On typical two's-complement hardware you often see a wrapped-looking negative like
-294967296(i.e. ), but you must not rely on this — the point is the result is not the correct positive sign the sort needs. Why this step? The comparator would then claim "a < b" when trulya > b, silently corrupting the sort. - The safe form.
(a>b)-(a<b)=(1)-(0) = +1→ correctly positive, no arithmetic on the values themselves, no overflow, no UB. Why this step? It computes the sign directly from booleans (each0or1), which can never overflow.
Verify: true value 4000000000 overflows INT_MAX = 2147483647 (UB in C); safe form = +1 (correct). Sanity: 2e9 really is greater than -2e9, so any correct comparator returns positive. ✓
Ex 7 — Signature mismatch: the wrong-type call (Cell G2)
double scale(double x) { return x * 2.0; } // takes/returns double
int (*p)(int); // pointer to int(int)
p = (int (*)(int)) scale; // force-cast the address in
int r = p(5); // call through the WRONG typeIs this defined? What actually goes wrong at the call?
Forecast: Guess — since scale's address fits in p, will p(5) just work?
- The address does copy fine. A function's address is just a number; the cast in
p = (int(*)(int))scale;compiles. Why this step? This is why the mistake feels safe — the assignment doesn't complain. - But the type is the calling contract.
int(*)(int)tells the CPU: "push oneintargument, and read the return value out of the integer register."scaleexpects adoublein a floating-point register and returns adoublefrom one. Why this step? The type — not the address — decides how bytes are passed and read back. Mismatch means the machine reads the wrong registers. - Calling through the mismatched pointer is undefined behaviour.
p(5)may return garbage, corrupt the stack, or crash. The C standard says calling a function through a pointer of an incompatible type is UB — full stop. Why this step? There is no "usually works" here; you cannot predict or rely on any output. - The fix. Never cast a function pointer to a different signature to call it. Match return type and every parameter exactly: use
double (*p)(double); p = scale;. Why this step? A matching type restores the correct calling contract.
Verify: the assignment compiles (address fits); the call p(5) is UB (no defined result). Sanity check the correct version instead: scale(5.0) = 5.0 * 2.0 = 10.0, which is what you get only through a properly-typed double(*)(double) pointer. ✓
Ex 8 — NULL / uninitialised pointer (Cell H)
int (*p)(int,int); // uninitialised — holds garbage
// ... p never assigned ...
int r = p(1, 2); // ??What happens, and what's the disciplined fix?
Forecast: Guess the outcome of calling p here.
- Diagnose the value of
p. An uninitialised local pointer holds an indeterminate bit pattern — some random address. Why this step? Calling means "jump to this address and execute." A garbage address points at data, unmapped memory, or nowhere. - The call.
p(1,2)transfers control to that garbage address → undefined behaviour, typically a segmentation fault / crash. Why this step? There's no valid function there; the CPU executes nonsense or hits a protection fault. - Fix step 1 — initialise.
int (*p)(int,int) = NULL;gives a known invalid value. Why this step?NULLis a sentinel you can test for, unlike random garbage. - Fix step 2 — guard.
if (p) r = p(1,2);so you never call throughNULL. Why this step? A checked NULL turns a crash into a controlled skip.
Verify: uninitialised call → UB/crash (not a value we can compute — that's the whole danger). Fixed pattern: p == NULL → skip → no call. Sanity: the guard reduces to if (p != NULL). ✓
Ex 9 — Real-world word problem (Cell I)
A heating library exposes:
// calls the callback once per reading, passing the temperature in °C
void monitor(int *readings, int n, void (*on_reading)(int));You want it to print an alert only when temperature exceeds 30°C. Readings: {22, 31, 18, 45}. How many alerts print?
Forecast: Guess how many alerts fire for {22, 31, 18, 45}.
- Write the callback (your behaviour).
Why this step? The library owns the timing (once per reading); you own the action. This is the callback split — behaviour as data.void alert(int t) { if (t > 30) printf("ALERT %d\n", t); } - Hand it over.
monitor(readings, 4, alert);passesalert's address (name decays). Why this step?monitorwill "call you back" on each reading without knowing what your code does. - Trace the four calls.
22→ no (22≤30);31→ alert;18→ no;45→ alert. Why this step? Only readings strictly above30trigger theprintf. - Count. Two readings satisfy
t > 30. Why this step? That's the answer the question asks for.
Verify: alerts printed = 2 (for 31 and 45). Sanity: readings > 30 are exactly {31, 45} → count 2. ✓ Swap alert for a log() callback and behaviour changes with zero edits to monitor — the point of callbacks.
Ex 10 — Exam twist: pointer to function returning a pointer (Cell J)
int *(*f)(int, int);Describe exactly what f is. Then contrast it with int (*g)(int,int).
Forecast: Is f a function returning a pointer, a pointer to a function, or something else? Guess first.
- Find the identifier. It's
f. Why this step? Reading C declarations starts at the name and works outward. - Innermost binding.
(*f)— the parentheses makefa pointer first (star hugs the name). Why this step? Without the parens (int *f(int,int)) it'd be "function returningint*" — the parens force pointer-first. - Apply the parameter list.
(int,int)→ the thingfpoints to is a function taking(int,int). Why this step? After establishing "pointer to function," the arg list tells us its signature. - Apply the return type.
int *→ that function returns a pointer toint. Why this step? The leftmostint *is the return type of the pointed-at function. - Full reading:
fis a pointer to a function taking twoints and returningint*. Why this step? Assemble the outward-read pieces into one English sentence. - Contrast.
int (*g)(int,int)returns a plainint;freturns anint*. Different return types → incompatible pointers; you cannot assign one to the other. Why this step? Return type is part of the type contract (Ex 7) — mismatched return types make the two pointers non-interchangeable.
Verify: f : int* (*)(int,int); g : int (*)(int,int). Sanity check with the typedef trick — typedef int* (*RetPtr)(int,int); then RetPtr f; reads cleanly and confirms the return type is int*. ✓
Recall Matrix coverage check
Which cells did we cover, A–J? ::: All of them: A(Ex1), B&C(Ex2), D(Ex3), E(Ex4), F(Ex5), G1(Ex6), G2(Ex7), H(Ex8), I(Ex9), J(Ex10). No cell left unshown.
Sign, count, aim. Every function-pointer scenario is one of: what sign does the comparator return (order), how many times is the callback called (0 for degenerate inputs), and does the pointer aim at real code with the right type (else crash / UB).
Connections
- Parent topic ↩
- qsort and the C Standard Library — Ex 2, 3, 4 live here.
- void pointers — why the comparator takes
const void *. - Arrays in C — the dispatch table of Ex 5.
- typedef — taming the Ex 10 declaration.
- Polymorphism — one call site, many behaviours (Ex 2, 5, 9).
- Pointers in C — the base idea behind all of it.