5.1.13 · D5C Programming
Question bank — Function pointers — declaration, calling, use in callbacks
Before you start, one vocabulary refresher so no term below ambushes you:
True or false — justify
Function names and &function are the same value
True. A function name decays to its address, so
add and &add yield the same pointer with type int(*)(int,int); the & is optional and purely cosmetic.int *p(int,int); declares a pointer to a function
False.
() binds tighter than *, so this declares a function named p that takes two ints and returns int*. You need int (*p)(int,int) to get the pointer.p(3,4) and (*p)(3,4) can give different results
False. Dereferencing a function pointer yields a function that immediately decays back to a pointer, so C treats both call forms as identical — same result, same generated code.
A function pointer stores the address of a variable that holds the function
False. It stores the address of the code itself — the memory location where the function's machine instructions begin. There is no extra variable in between.
Two functions with the same body but different parameter types share a function-pointer type
False. The type is fixed by the signature, not the body.
int f(int) and int g(double) need different pointer types even if their code looks alike.You can safely store any function's address in a void* and call it later
False. The standard only guarantees function pointers convert cleanly to other function-pointer types, not to
void* (a data pointer). Mixing the two is not portable — that is why qsort uses int(*)(const void*,const void*), a function-pointer type, for its comparator.typedef int (*BinOp)(int,int); creates a new function
False. typedef creates a new type name, not a function or a variable.
BinOp is just a shorter alias for the pointer type; no code is generated.Adding a fourth entry to int (*ops[3])(int,int) is fine as long as you index carefully
False. The array has exactly 3 slots; writing
ops[3] = ... is out-of-bounds and is UB. Resize the array (or use a larger size) — careful indexing cannot rescue a too-small array.An int(*)(int,int) and an int(*)(const int,const int) are incompatible types
False (subtle). Top-level
const on a parameter is ignored in a function type, so these two signatures are considered the same and are assignment-compatible.Spot the error
int (*p)(int,int); int r = p(3,4); — what's wrong?
p was declared but never assigned, so it holds garbage. Calling it jumps to a random address → crash / UB. Initialise it (p = add; or at least = NULL and check) first.int (*p)(int) = add; where int add(int,int) — what's wrong?
Signature mismatch:
p expects one parameter, add takes two. The types don't match, so calls set up the stack wrong → UB. The pointer type must mirror add exactly: int (*p)(int,int).Comparator return a - b; in qsort — why is it a trap?
For extreme values
a - b can overflow and flip its sign, silently breaking the sort. qsort only needs the sign, so use (a>b)-(a<b), which never overflows.int cmp(int a, int b){...} passed to qsort — what's wrong?
qsort's comparator must be
int(*)(const void*,const void*). A comparator taking two ints has the wrong signature; qsort actually passes addresses (const void*), so you must cast them inside: int x = *(const int*)a;.void (*f)(void) = add; f(); where add returns int — what's wrong?
Return-type mismatch. Even ignoring the value,
f's type says "returns nothing" while add returns int; the call convention for reading the result differs, so this is UB, not merely a discarded value.ops[opcode](x,y) with opcode from user input — what's the hidden bug?
No bounds check. If
opcode exceeds the array's last valid index (or is negative), you read a garbage pointer and call through it → UB. Validate opcode against the array length before dispatching.Why questions
Why must the parentheses hug the name in int (*p)(int,int)?
Because
() (the "call" operator) binds tighter than *. Without the parentheses the compiler attaches * to the return type, declaring a function that returns a pointer instead of a pointer to a function.Why does the type of a function pointer include the parameter types at all?
Because the compiler must know how to set up each call: how many arguments, their sizes, where to place them, and how to interpret the returned value. The type is the call recipe, not just an address.
Why does qsort take a function pointer instead of a fixed comparison?
So the behaviour (how to compare) becomes data you pass in. One qsort implementation can sort ascending, descending, by any field — you supply the rule as a callback (see Polymorphism, the same idea in other languages).
Why does a comparator receive const void* rather than int*?
qsort must sort arrays of any element type, so it can't name a specific type in its signature. It uses void* (a type-agnostic address) and trusts you to cast back to the real type inside your comparator.
Why does casting const void *a to const int * and dereferencing work?
Because you know the array holds
ints (you told qsort via sizeof(int)). The cast tells the compiler "treat this address as pointing to an int," restoring the type information that void* erased. See Pointers in C for the mechanics.Why prefer an array of function pointers over a long switch?
Indexing
ops[opcode](x,y) is O(1) constant-time dispatch and trivially extensible — add a row, no code rewrite. A switch grows linearly and must be edited for every new case. This is how interpreters and VMs dispatch opcodes.Why does typedef make function-pointer code safer, not just shorter?
It names the type once, so every later use reads like an ordinary variable (
BinOp f). This removes the chance of re-writing the fiddly (*...) syntax wrong in each declaration and parameter.Edge cases
What happens if you call through a NULL function pointer?
Undefined behaviour — typically an immediate crash (jump to address 0).
NULL is a safe sentinel to test against (if (p) p();), but it is never itself callable.Is sizeof(int (*)(int,int)) the same as sizeof(int)?
No, and don't assume it equals a data-pointer size either. A function-pointer size is implementation-defined and may differ from
void* on exotic architectures; never rely on it being any particular number.Can two different function pointers compare equal?
Yes — if both were assigned the address of the same function,
p == q is true. Comparing pointers to different functions gives false; that's the only meaningful comparison for function pointers.Can a function pointer point to a function that itself takes a function pointer?
Yes. Nothing forbids it — a comparator, an event registrar, or a higher-order routine can all be pointed to. The type just nests: e.g.
void (*)(int(*)(int,int)), which is exactly why typedef exists.What if the pointed-to array of function pointers is only partly initialised, e.g. int (*ops[3])(int,int) = { add };?
The remaining elements are set to
NULL (aggregate zero-initialisation of the rest). ops[0] is safe; calling ops[1] or ops[2] calls through NULL → UB. Always fill or guard every slot.Does an empty parameter list int (*p)(); mean "takes no arguments"?
No — in C, empty parentheses in this form mean "unspecified arguments," so the compiler skips argument-count checking, which is a trap. Write
int (*p)(void) to genuinely mean "takes nothing."Can you take the address of a function pointer variable itself?
Yes.
p is an ordinary variable that lives in memory, so &p gives a pointer to a function pointer of type int (*(*)(int,int)) — useful when a routine must reassign which callback you use.Connections
- Parent topic — the full derivation these traps stress-test.
- Pointers in C, void pointers, Arrays in C, typedef — the building blocks reused above.
- qsort and the C Standard Library, Polymorphism — where callbacks pay off.