Exercises — Function pointers — declaration, calling, use in callbacks
Level 1 — Recognition
Exercise 1.1
Which of these declares a pointer to a function taking two ints and returning int?
- (A)
int *p(int, int); - (B)
int (*p)(int, int); - (C)
int (p*)(int, int); - (D)
int *(p)(int, int);
Recall Solution 1.1
The answer is (B).
- (A)
int *p(int,int)— the()afterpbind first, sopis a function returningint*. Not a pointer. - (C)
int (p*)(...)—p*is not valid syntax; the*must come before the name. - (D)
int *(p)(int,int)— the extra parens aroundpalone do nothing; this is still a function returningint*(same as A).
Only (B) puts * and the name together inside parentheses: (*p). Read inside-out: p is a pointer, (*p)(int,int) is a call, returning int. Star hugs the name, parens make the game.
Exercise 1.2
Given int add(int, int);, mark each as true or false:
addhas typeint(*)(int,int)when used inp = add;&addandaddproduce different addresses.(*add)is a syntax error.
Recall Solution 1.2
- True. A function name decays to its address in most expressions, so
addhere is anint(*)(int,int)value. - False.
&addand the decayedaddare the same address — a function has exactly one entry point. - False.
*addfirst decaysaddto a pointer, then dereferences it back to a function — perfectly legal (it just re-decays again if you try to call it).
Exercise 1.3
What is the type of cmp in qsort's signature? Write it exactly.
Recall Solution 1.3
It is a pointer to a function taking two const void * (see void pointers) and returning int (the sign tells qsort the ordering).
Level 2 — Application
Exercise 2.1
Given
int add(int a,int b){ return a+b; }
int sub(int a,int b){ return a-b; }Write the code to declare a function pointer p, point it at sub, and call it with (9, 4). Give the result.
Recall Solution 2.1
int (*p)(int, int); // declare
p = sub; // sub decays to &sub
int r = p(9, 4); // call through pointersub(9,4) = 9 - 4 = 5. Both p(9,4) and (*p)(9,4) give the same 5.
Exercise 2.2
Complete the comparator so qsort sorts an int array ascending, then predict the output of sorting {5, 2, 8, 1}.
int cmp(const void *a, const void *b) {
int x = /* ??? */;
int y = /* ??? */;
return /* ??? */;
}Recall Solution 2.2
int cmp(const void *a, const void *b) {
int x = *(const int *)a; // reinterpret the void* as int*, then read
int y = *(const int *)b;
return (x > y) - (x < y); // -1, 0, or +1 — safe sign, no overflow
}Sorting {5,2,8,1} ascending gives =={1, 2, 5, 8}==.
Why the cast? qsort doesn't know your element type — it hands you const void * (see void pointers). You know they're int, so you cast then dereference.
Exercise 2.3
Using the same elements {5,2,8,1}, what comparator body produces descending order, and what is the sorted result?
Recall Solution 2.3
Flip the roles of x and y:
return (y > x) - (y < x); // reverse orderingResult: =={8, 5, 2, 1}==. Only the returned sign flipped; the qsort call is unchanged.
Level 3 — Analysis
Exercise 3.1
Trace this program and give the printed number.
int add(int a,int b){ return a+b; }
int mul(int a,int b){ return a*b; }
int (*ops[2])(int,int) = { add, mul };
int r = ops[1](ops[0](2,3), 4);
printf("%d\n", r);Recall Solution 3.1
Inner call first: ops[0](2,3) = add(2,3) = 5.
Outer call: ops[1](5, 4) = mul(5,4) = ==20==.
The array ops is a dispatch table: ops[0] is add, ops[1] is mul. Indexing selects the behaviour, then (...) calls it. See Arrays in C.
Exercise 3.2
For the comparator return a - b; on 32-bit int, find two values that make it return the wrong sign, and explain why. (Assume int overflow wraps mod .)
Recall Solution 3.2
Take a = 2000000000, b = -2000000000. Mathematically a > b, so a correct comparator returns positive.
But a - b = 2000000000 - (-2000000000) = 4000000000, which exceeds INT_MAX = 2147483647. It wraps to
a negative result — the wrong sign. qsort now thinks a < b and the sort is corrupted.
This is exactly why we use (a>b) - (a<b): it only ever yields -1/0/+1, never overflows.
Exercise 3.3
Why do p(3,4) and (*p)(3,4) compile to the identical call? Explain in terms of decay.
Recall Solution 3.3
A function call needs a function on its left. p is a pointer, so it decays to the function it points at → callable directly: p(3,4).
*p dereferences the pointer to get the function itself; but a function immediately decays back to a pointer whenever you try to use it — and a call decays it once more to reach the function. Net effect: both spellings land on the same function with the same call. C deliberately accepts either. (**p)(3,4), (***p)(3,4), … all work too, for the same reason.
Level 4 — Synthesis
Exercise 4.1
Using typedef, define a type BinOp for "pointer to int(int,int)", build a 4-entry dispatch table for add, sub, mul, div_op, and write a one-line call that computes 20 / 5 through the table. Assume the table order is {add, sub, mul, div_op}.
Recall Solution 4.1
typedef int (*BinOp)(int, int);
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 div_op(int a,int b){ return a/b; }
BinOp ops[4] = { add, sub, mul, div_op };
int r = ops[3](20, 5); // div_op(20,5)div_op(20,5) = 20 / 5 = ==4==. Index 3 is div_op; the typedef makes BinOp ops[4] read like an ordinary array.
Exercise 4.2
Write a function apply_all that takes an int array, its length, and a BinOp f, and folds the array left-to-right: result = f(f(f(a[0],a[1]),a[2]), …). Then compute the result of applying add to {1,2,3,4} and applying mul to {1,2,3,4}.
Recall Solution 4.2
int apply_all(int *arr, int n, BinOp f) {
int acc = arr[0];
for (int i = 1; i < n; i++)
acc = f(acc, arr[i]);
return acc;
}addfold:((1+2)+3)+4 ===10==.mulfold:((1*2)*3)*4 ===24==.
The loop is fixed; the behaviour is passed in as data. This is the essence of a callback and a step toward Polymorphism.
Exercise 4.3
Sort the array {{ "banana", 3 }, { "apple", 1 }, { "cherry", 2 }} (a struct with a char *name and int rank) by rank ascending using qsort. Write the comparator and give the resulting order of names.
Recall Solution 4.3
typedef struct { char *name; int rank; } Item;
int by_rank(const void *a, const void *b) {
const Item *x = a; // void* implicitly converts to Item*
const Item *y = b;
return (x->rank > y->rank) - (x->rank < y->rank);
}
qsort(items, 3, sizeof(Item), by_rank);Ranks are banana=3, apple=1, cherry=2. Ascending by rank →
==apple (1), cherry (2), banana (3)==.
qsort handles arbitrary element sizes via the sizeof(Item) argument; your comparator interprets the raw bytes.
Level 5 — Mastery
Exercise 5.1
Is this defined behaviour? Explain precisely.
double half(double x){ return x/2; }
int (*p)(int, int);
p = (int(*)(int,int)) half; // cast the address
int r = p(10, 4);Recall Solution 5.1
Undefined behaviour. The cast of the pointer is allowed — converting one function-pointer type to another is legal. What is not allowed is calling through a pointer whose type doesn't match the function's actual type.
half expects one double (passed in a floating-point register, typically) and returns a double. Calling it as int(int,int) makes the CPU push two integer arguments and read an integer return — the calling conventions don't line up, so you get garbage or a crash.
Rule: you may store/cast a mismatched function pointer, but you must convert it back to the correct type before calling. (Signature mismatch is one of the parent note's four core mistakes.)
Exercise 5.2
What does calling an uninitialized function pointer do, and how does = NULL help if you never assigned it?
int (*p)(int,int); // uninitialized
if (p) p(1,2);Recall Solution 5.2
Uninitialized, p holds whatever garbage was on the stack. if (p) is almost always true (garbage is rarely exactly 0), so p(1,2) jumps to a random address → crash or arbitrary code, all undefined behaviour.
Initializing int (*p)(int,int) = NULL; makes if (p) reliably false until you assign a real function, so the guard actually protects you.
Rule: initialize function pointers to NULL and check before every call you're not certain is assigned.
Exercise 5.3
Predict the exact output.
int add(int a,int b){ return a+b; }
int sub(int a,int b){ return a-b; }
int (*ops[2])(int,int) = { add, sub };
int i = 0;
int r = ops[i++](ops[i](7, 2), 1);
printf("%d\n", r);Recall Solution 5.3
This is a sequence-point subtlety. The expression reads i (as ops[i]) and modifies it (i++) with no sequence point between them when the two i subexpressions are unsequenced → this is undefined behaviour in C. The compiler may evaluate ops[i] before or after i++.
So there is no single correct number — the standard permits any result (or a crash).
If it were written safely — e.g. int inner = ops[1](7,2); int r = ops[0](inner, 1); — then:
- inner:
sub(7,2) = 5 - outer:
add(5,1) ===6==. The lesson: function pointers don't rescue you from the ordinary rules of C evaluation — never read and modify the same object without a sequence point.
Connections
- Parent: Function Pointers — this page drills its ideas.
- Pointers in C · void pointers · Arrays in C · typedef · qsort and the C Standard Library · Polymorphism