C in 100 Seconds: Function Pointers | Episode 27
Video: C in 100 Seconds: Function Pointers — Callbacks and Dispatch | Episode 27 by Taught by Celeste AI - AI Coding Coach
C Function Pointers: Callbacks and Dispatch
int (*fp)(int, int) = add;—fpis a pointer to a function taking two ints and returning an int. Pass it as a parameter for callbacks; store an array for dispatch tables.
Function pointers are how C does callbacks, plugins, and runtime dispatch. The syntax is dense; once you know the pattern, the rest is straightforward.
The basic shape
#include <stdio.h>
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; }
void apply(int (*op)(int, int), int x, int y) {
printf("Result: %d\n", op(x, y));
}
int main() {
int (*fp)(int, int);
fp = add;
printf("add: %d\n", fp(10, 3)); // 13
fp = sub;
printf("sub: %d\n", fp(10, 3)); // 7
apply(mul, 10, 3); // Result: 30
apply(add, 5, 7); // Result: 12
return 0;
}
The syntax
int (*fp)(int, int);
// ^^^ ^^^^^^^^
// | parameter types
// pointer name
// return type
Read as: "fp is a pointer to a function taking (int, int) and returning int."
The parens around *fp are required. Without them:
int *fp(int, int); // function returning int*, NOT a function pointer
The two are completely different. Always write int (*fp)(int, int) for function pointers.
Assigning a function
int add(int a, int b) { return a + b; }
int (*fp)(int, int) = add; // fp now points to add
int (*fp2)(int, int) = &add; // same — & is implicit for function names
Function names in C automatically decay to function pointers when used in a context that expects one. So add and &add are equivalent.
Calling through a pointer
int result = fp(10, 3); // call as if fp were the function
int result2 = (*fp)(10, 3); // explicit dereference; same effect
Calling syntax is the same as for direct function calls. Some old C code uses (*fp)(...); modern style is just fp(...). Both work.
Passing as a parameter (callback)
void apply(int (*op)(int, int), int x, int y) {
printf("%d\n", op(x, y));
}
apply(add, 10, 3); // pass add as the "op"
apply(mul, 10, 3);
The function takes a function pointer plus the args. Inside, it calls the pointed-to function.
This is how qsort works:
#include <stdlib.h>
int compare(const void *a, const void *b) {
int ia = *(int*)a;
int ib = *(int*)b;
return ia - ib;
}
int nums[] = {3, 1, 4, 1, 5, 9, 2, 6};
qsort(nums, 8, sizeof(int), compare);
qsort doesn't know how to compare your data — you provide the comparator function. Pure C polymorphism.
typedef for clarity
The syntax is dense; typedef makes it manageable:
typedef int (*MathOp)(int, int);
MathOp fp = add;
void apply(MathOp op, int x, int y) { ... }
Now MathOp is a clear name. Function-pointer typedef is the canonical typedef use case.
Dispatch tables
typedef int (*MathOp)(int, int);
MathOp ops[] = { add, sub, mul };
const char *names[] = { "add", "sub", "mul" };
for (int i = 0; i < 3; i++) {
printf("%s(5, 2) = %d\n", names[i], ops[i](5, 2));
}
An array of function pointers, indexed by operation. Cleaner than a chain of if (op == "add") ... else if (op == "sub") ....
For "registered handlers" patterns — event loops, command dispatchers, parser actions — dispatch tables scale better than if-else.
Returning function pointers
typedef int (*MathOp)(int, int);
MathOp pick_op(char op) {
switch (op) {
case '+': return add;
case '-': return sub;
case '*': return mul;
default: return NULL;
}
}
MathOp f = pick_op('+');
if (f) printf("%d\n", f(5, 3));
Without typedef, the return type would be int (*)(int, int) written awkwardly. Typedef strongly recommended for return types.
Closures? Not in C
C doesn't have closures. Function pointers are just function addresses — no captured variables. To carry context, use a "context pointer" alongside:
typedef int (*Callback)(void *ctx, int data);
void process(Callback cb, void *ctx, int data) {
cb(ctx, data);
}
The caller passes both the function pointer and a context pointer. Common pattern in C APIs.
Function pointers vs raw memory pointers
Function pointers are not the same as data pointers:
void (*fp)(void) = NULL;
void *dp = NULL;
fp = (void(*)(void))dp; // technically undefined behavior on some platforms
On most modern systems they're the same size, but the C standard doesn't guarantee it. POSIX systems guarantee void * and function pointers are interconvertible (for dlsym to work). Stick to typed function pointers when you can.
Common patterns
Event handler:
struct EventHandler {
void (*on_click)(int x, int y);
void (*on_key)(char c);
};
State machine:
typedef void (*StateFn)();
void state_idle() { /* ... */ }
void state_running() { /* ... */ }
StateFn current_state = state_idle;
current_state(); // dispatch
Plugin / dynamic library:
#include <dlfcn.h>
void *handle = dlopen("plugin.so", RTLD_LAZY);
int (*plugin_fn)(int) = dlsym(handle, "process");
dlsym returns a function pointer; you cast to the appropriate type.
Common mistakes
Missing parens. int *fp(int, int) is "function returning int pointer." int (*fp)(int, int) is "pointer to function." Always parenthesise.
Wrong type signature. Function pointers must match exactly. int(*)(int,int) is not assignable from int(*)(double,double).
Calling NULL. MathOp f = NULL; f(1, 2); segfaults. Always check.
Function pointers in printf. printf("%p", fp); — non-portable; some systems require casting first: printf("%p", (void*)fp);.
Overcomplication. For one-off "if this then call that," plain if/switch is clearer than function pointer arrays.
What's next
Episode 28: arrays of pointers — string lists and pointer tables. Storing multiple strings as char *[].
Recap
int (*fp)(int, int) = add; — function pointer. Call as fp(...) or (*fp)(...) (same). Pass as parameter for callbacks; store in arrays for dispatch tables; return for "factory of behaviours." Use typedef to give the type a readable name. C has no closures — pair with a context pointer for stateful callbacks. Function pointers and data pointers aren't always interconvertible by the standard.
Next episode: arrays of pointers.