C / POINTERS
Function pointers and dispatch tables
Store functions in variables and tables so data picks the code that runs, and replace switch chains with an indexed dispatch table.
What you will learn
- Declare int (*f)(int, int) and say why dropping the parentheses changes the meaning
- Build an array or struct table of handlers that all share one exact signature
- Replace a switch chain with a table lookup, range-checking the key before the call
- Pass a comparator to qsort and keep its signature identical to what qsort expects
Understanding Function pointers and dispatch tables
Every function in a compiled program lives at some address, and writing the function's name in an expression without the call parentheses gives you that address: `add` and `&add` are the same value, of type `int (*)(int, int)`. The declaration needs the star in parentheses, `int (*f)(int, int)`, because without them the star binds to the return type and you have declared a function returning `int *` instead. Parameter and return types are part of the pointer's type, and that is what lets the compiler check each call and place the arguments where the callee expects them.
Given the pointer, `f(7, 3)` calls through it, and the older spelling `(*f)(7, 3)` means exactly the same thing, because the call operator wants a function pointer and applying `*` to one yields a function that decays straight back to a pointer. What you cannot do is treat it like a pointer to data: there is no defined object to read at that address, `f + 1` is not arithmetic the language allows, and converting it to `void *` is outside what the standard promises. Copying, comparing and calling are the whole set of operations.
A dispatch table is what you get when those addresses are stored in a data structure keyed by whatever decides the behavior: an array indexed by a small integer, or rows of key-plus-handler that you search. A `switch` puts that mapping into control flow, so a new case means editing a function; a table puts it into data, so a new case means one more row and the dispatch code never changes. The call cost drops from a chain of comparisons to one index and one indirect jump, but you inherit two duties the `switch` handled for you: check the key before indexing, and make sure the slot you found is not NULL, because there is no `default:` left to catch an unknown key.
<stdio.h>
static int add(int a, int b) { return a + b; }
static int sub(int a, int b) { return a - b; }
static int mul(int a, int b) { return a * b; }
int main(void)
{
int (*ops[3])(int, int) = { add, sub, mul };
const char *names[3] = { "add", "sub", "mul" };
int (*chosen)(int, int) = ops[2];
for (int i = 0; i < 3; i++)
printf("%s(7, 3) = %d\n", names[i], ops[i](7, 3));
printf("chosen == mul: %d\n", chosen == mul);
printf("(*chosen)(7, 3) = %d\n", (*chosen)(7, 3));
return 0;
}
A function's address is an ordinary value, so which code runs can be selected by data in a table instead of by branches written into the source.
Worked examples
A comparator handed to qsort
Passing behavior into a library function that was compiled long before your comparison rule existed.
<stdio.h>
<stdlib.h>
static int by_desc(const void *a, const void *b)
{
int x = *(const int *)a;
int y = *(const int *)b;
return (x < y) - (x > y);
}
int main(void)
{
int v[6] = { 4, 11, 2, 9, 7, 1 };
qsort(v, 6, sizeof v[0], by_desc);
for (int i = 0; i < 6; i++)
printf(i ? " %d" : "%d", v[i]);
putchar('\n');
return 0;
}
Example explained
Line 1qsort's fourth parameter has type int (*)(const void *, const void *), so by_desc must match that signature exactly; calling through a mismatched type is undefined behavior.
Line 2by_desc is written without parentheses, so the argument is the function's address rather than the result of calling it.
Line 3(x < y) - (x > y) returns -1, 0 or 1 without the overflow risk of x - y, and putting the smaller test first is what makes the order descending.
Line 4qsort itself contains no comparison logic for ints; the element size and this one pointer are the only behavior you supply.
Keyed table with a sentinel row
A command dispatch table searched by name, where an unknown key produces NULL instead of a wrong call.
<stdio.h>
<string.h>
struct command {
const char *name;
void (*run)(const char *arg);
};
static void say_upper(const char *arg)
{
for (const char *p = arg; *p != '\0'; p++)
putchar(*p >= 'a' && *p <= 'z' ? *p - 'a' + 'A' : *p);
putchar('\n');
}
static void say_len(const char *arg)
{
printf("%zu\n", strlen(arg));
}
static const struct command table[] = {
{ "upper", say_upper },
{ "len", say_len },
{ NULL, NULL }
};
static const struct command *lookup(const char *name)
{
for (const struct command *c = table; c->name != NULL; c++)
if (strcmp(c->name, name) == 0)
return c;
return NULL;
}
int main(void)
{
const char *words[] = { "upper", "len", "reverse" };
for (int i = 0; i < 3; i++) {
const struct command *c = lookup(words[i]);
if (c == NULL)
printf("no command %s\n", words[i]);
else
c->run("hello there");
}
return 0;
}
Example explained
Line 1Each row pairs a key with a handler, so adding a command means adding a row; lookup and main are never touched.
Line 2The { NULL, NULL } row is the sentinel that stops the search loop, so lookup needs no separate length constant to stay in step with the table.
Line 3c->run("hello there") calls through the struct member, and the member's declared type void (*)(const char *) is what the compiler checks the argument against.
Line 4lookup returns NULL for "reverse", and the NULL test in main is doing the job a switch would have given to default:.
Important notes
A function pointer is not an object pointer. The standard only guarantees converting a function pointer to another function pointer type and back, so do not park callbacks in void * fields even though it happens to work on common systems.
Casting a pointer to one signature and calling it as another is undefined behavior even when the arguments look compatible; change the shared signature instead of casting the pointer.
Common mistakes
Writing int *op(int, int); when a pointer was meant: that declares a function returning int *, so op = add; is rejected as an assignment to a non-lvalue rather than working as intended.
Putting call parentheses in the initializer, as in { add(), sub() }: those are calls, so the compiler reports too few arguments or an int being used to initialize a function pointer.
Indexing straight from input, as in ops[c - '0'](a, b) with no range test: an out-of-range slot reads whatever bytes follow the array and calls them as code, which segfaults at best and is the classic way a dispatch table becomes an exploit.
Try it yourself
Change, predict, then run
Add a divide handler to the main example's table, then write int dispatch(char op, int a, int b, int *out) that maps '+', '-', '*', '/' to an index and returns 0 without indexing the array for any other character. Confirm dispatch('%', 7, 3, &r) reports failure instead of calling something.
Open the C workspaceCheck your understanding
You replace a 12-case switch on a command byte with a 256-entry table of function pointers. What best describes how the correctness risk changed?
- Nothing changes: a table is a faster switch, and the compiler still verifies that every possible byte maps to a real handler.
- The default case disappears, so an unknown byte now lands on a slot that must be in range and non-NULL or the call is undefined behavior.
- Indexing is safer than a switch, because C checks the array bounds before performing the indirect call.
- The handlers' signatures stop mattering, since each slot stores only an address.
Show answer
The switch gathered every unhandled byte into one default: branch; the table moves that responsibility into the initializer plus an explicit range and NULL check, and a bad slot is jumped to as if it were code. Option 2 inverts the risk: array indexing is precisely the operation C does not check. Option 3 is also wrong, because the slot's declared type is what makes the call legal at all.