Visual walkthrough — Function pointers — declaration, calling, use in callbacks
We only assume you have met plain pointers (an arrow that stores where something lives) and arrays (a row of same-sized boxes). Everything else we build here.
Step 1 — Code lives in memory, just like data
WHAT. When you compile a program, two very different things end up in the computer's memory:
- your data (numbers, arrays) — the values you push around, and
- your code (the actual machine instructions of each function).
Both sit at numbered locations. A location's number is called its address.
WHY. A normal pointer is exciting because it stores the address of data. The whole trick of this chapter is realising that a function also has an address — the memory location where its first instruction begins. If data addresses can be stored in a variable, so can code addresses. That variable is a function pointer.
PICTURE. Memory is one long strip of numbered cells. The data region holds our array {3,1,2}. The code region holds the instructions of add. Each has a starting address (the green tag).
Step 2 — A function name decays to its address
WHAT. Write a real function:
int(first) — the return type:addhands back anint.add— the name, which the compiler ties to a starting address.(int a, int b)— the parameter list: twoints go in.
Now the surprise: in almost every expression, the bare name add does not mean "run add" — it decays into add's address.
WHY. This is the same rule you already met with arrays: an array name decays to a pointer to its first element (see Arrays in C). C reuses that habit for functions so you can grab a function's address just by naming it. That is what makes assigning a function to a variable feel effortless.
PICTURE. The name add is a label taped onto the door of the code block. Reading the label (using add in an expression) gives you the room number — the address — not the furniture inside.
Step 3 — Build the pointer's type by copying the function's shape
WHAT. A function pointer must store not just an address but an address of a specific shape of function. Its type has to encode:
- the return type, and
- the exact parameter types.
We build the declaration by transforming the function's own declaration:
We swapped the name add for (*p): "p is a pointer".
WHY. The compiler needs the full shape so it can set up a call correctly — how many arguments, what sizes they are, and how big a value comes back. A pointer that lost this information would be useless at call time. So the type is the safety label: mismatch it and behaviour is undefined (see the parent note's "signature mismatch" mistake).
WHY the parentheses. Look at what happens without them:
The call-parentheses () bind tighter than *, so * would attach to the return type, not to p. Wrapping (*p) forces * to hug p first, making p the pointer; only then do the outer (int,int) apply.
PICTURE. Two parse trees. On the left, int *p(int,int): p is the function, * floats up to the return. On the right, int (*p)(int,int): the parentheses cage *p together so p is the pointer, and the function-shape wraps around it.
Step 4 — Assign: copy the address into the pointer box
WHAT.
int (*p)(int, int); // an empty box shaped for int(int,int)
p = add; // add decays to its address; store it in pWHY. p is just a variable. Assignment copies the number (the address from Step 2) into p's box. Because the shapes match — both are int(int,int) — the compiler is happy. p = &add; does the identical thing; &add and add are the same value (Step 2).
PICTURE. The p box, previously empty, now holds an arrow pointing at the door of the add code block.
Step 5 — Call: follow the arrow, then run
WHAT. Two forms, identical result:
int r1 = p(3, 4); // 7
int r2 = (*p)(3, 4); // 7WHY both work. Calling needs a function. (*p) dereferences the pointer to get the function itself — but a function immediately decays right back to a pointer (Step 2). So p and *p collapse to the same thing at a call site. C accepts either spelling on purpose.
p— the pointer (holds the address).(*p)— "the functionppoints at".(3, 4)— the arguments packed and sent in.r1— the returnedint(here3 + 4 = 7).
PICTURE. Trace the arrow from p to the add block, the machine executes a + b, and the value 7 travels back out along the return path.
Step 6 — The callback: hand your button to qsort
WHAT. Now the payoff. qsort (from qsort and the C Standard Library) has this shape:
void qsort(void *base, size_t n, size_t size,
int (*cmp)(const void *, const void *));base— where the array starts.n— how many elements.size— bytes per element.cmp— a function pointer:qsortwill call your function to decide the order.
The void * parameters mean "an address to something, type not fixed here" — that is how one qsort sorts any type; your comparator casts them back to int*. (This type-erasure trick is Polymorphism in plain C.)
int cmp_asc(const void *a, const void *b) {
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y);
}
int arr[] = {3, 1, 2};
qsort(arr, 3, sizeof(int), cmp_asc); // arr -> {1, 2, 3}WHY (x>y)-(x<y). qsort only reads the sign of the return: negative means "a before b", positive means "a after b", zero means "equal". Each of (x>y) and (x<y) is 0 or 1, so the difference is exactly one of — the right sign, with no overflow (unlike x - y, which can wrap for extreme ints).
PICTURE. You pass cmp_asc's address into qsort. Inside its loop, whenever qsort needs to compare two elements, it reaches back out and calls you — the arrow points backward from library to your code. That backward arrow is what "callback" means.
Step 7 — All the sign cases must be covered
WHAT. A comparator must give a correct answer for every ordering of two values. Walk all three:
| case | (x>y) |
(x<y) |
result | meaning |
|---|---|---|---|---|
| 0 | 1 | a comes before b | ||
| 0 | 0 | keep as-is (equal) | ||
| 1 | 0 | a comes after b |
WHY. If you only handled two of the three (say you forgot the equal case), qsort's behaviour becomes unpredictable. Covering all three signs — including the degenerate "they're equal" case — is what makes the comparator total and the sort correct.
Descending is the same machine with the roles flipped:
int cmp_desc(const void *a, const void *b) {
int x = *(const int*)a, y = *(const int*)b;
return (y > x) - (y < x); // swap x and y => reverse order
}PICTURE. A number line with two markers x and y. Three panels — x left of y, on top of y, right of y — each showing the sign the comparator returns and the resulting arrow direction.
Step 8 — Many buttons: the dispatch table
WHAT. An array of function pointers replaces a switch:
typedef int (*BinOp)(int, int); // name the shape once (see [[typedef]])
BinOp ops[3] = { add, sub, mul };
int r = ops[1](10, 4); // -> 6, calls subBinOp— a readable alias forint(*)(int,int)(from typedef).ops— three boxes, each holding a code-address.ops[1]— pick box 1 (that'ssub).(10, 4)— call it →10 - 4 = 6.
WHY. Instead of an if/else chain that grows with every new operation, you index into behaviour: ops[opcode](x,y) is one clean, O(1) lookup. This is exactly how interpreters and virtual machines dispatch instructions.
PICTURE. A row of three boxes; each box's arrow lands on a different code block (add, sub, mul). The index 1 lights up box 1, and its arrow fires into sub.
The one-picture summary
Everything at once: a name decays to an address (Step 2) → the address is stored in a shaped pointer box (Steps 3–4) → following the arrow runs the code (Step 5) → handing that arrow to a library makes it call you back (Step 6) → a whole row of arrows becomes a dispatch table (Step 8).
Recall Feynman retelling — the whole walkthrough in plain words
Your functions are little rooms full of instructions, and every room has a number on the door. Say a function's name and C quietly hands you the room number, not the furniture. A function pointer is a labelled box you can write a room number into — but the box is shaped for a particular kind of room (this many arguments, this size answer), so you can't stuff the wrong room in. Once the number is in the box, you can "go to that room and run it" by following the arrow — and writing the star inside parentheses is what makes the box a pointer box instead of a room by accident. The magic trick is callbacks: you hand your box to a big helper like qsort, and instead of you calling it, it calls you every time it needs to compare two things — you decided the action, it decided the timing. And if you line up a whole row of these boxes, you get a dispatch table: pick box number 1, follow its arrow, run that behaviour — no giant if/else needed.
Recall
A function name in an expression gives you its ::: address (it decays, just like an array name).
Why must *p sit in parentheses in int (*p)(int,int)? ::: So * binds to p first; otherwise it declares a function returning int*.
p(3,4) vs (*p)(3,4) — result? ::: Identical; dereferencing a function pointer decays right back to a pointer.
What does (x>y)-(x<y) return for x==y? ::: 0 (both comparisons are false, so 0 - 0).
ops[1](10,4) with ops={add,sub,mul}? ::: 6 (calls sub).
Connections
- Parent topic — the full lesson.
- Pointers in C — the "arrow stores an address" idea we build on.
- Arrays in C — name-decay and the dispatch-table array.
- qsort and the C Standard Library — the flagship callback.
- void pointers — how
qsortstays type-agnostic. - typedef — naming the pointer shape.
- Polymorphism — one function, many behaviours via passed code.