A function pointer is a variable that holds the address of a function, instead of the address of data. Once the address is in a pointer, the function can be invoked by dereferencing the pointer. Function pointers turn static “decided at compile time” call sites into dynamic ones — the function actually called depends on whatever address is currently stored in the pointer.
Function pointers are how callbacks, dispatch tables, plugin systems, and many forms of polymorphism work in C — and underneath the language abstractions, in C++, Python, and almost every other language too.
Declaring and using
A function pointer’s type encodes the function’s signature: return type, parameter types. The syntax mimics a function declaration but parenthesises the pointer name:
int sum(int a, int b) { return a + b; }
int product(int a, int b) { return a * b; }
int (*op)(int, int); // op: pointer to function taking two ints, returning int
op = sum; // address-of operator '&' is optional
printf("%d\n", op(2, 3)); // prints 5 — same as (*op)(2, 3)
op = product;
printf("%d\n", op(2, 3)); // prints 6The two functions sum and product have the same signature int(int, int), so a single function pointer op can hold either. The call op(2, 3) invokes whichever function op currently points to.
Reading the syntax
The declaration int (*op)(int, int) is intimidating but mechanical. Two readings to memorise:
- Spiral: start at the variable name and read outward —
opis a pointer (*op) to a function ((int, int)) returning int. - Substitution: write the function declaration
int sum(int, int)and replace the function name with(*op).
Without the parentheses around *op, you’d be declaring int *op(int, int) — a function returning a pointer to int, which is something else entirely. The parentheses force the pointer-ness to bind to op.
typedef makes it readable
Cleaning up function-pointer declarations with typedef is almost always worth doing:
typedef int (*BinaryIntOp)(int, int);
BinaryIntOp op = sum;
op(2, 3); // 5
BinaryIntOp ops[] = { sum, product }; // array of function pointers
for (int i = 0; i < 2; i++) {
printf("%d\n", ops[i](2, 3)); // 5, 6
}BinaryIntOp reads as a normal type name, and the noise of the function-pointer syntax disappears from the variable declarations. Array-of-function-pointers patterns become legible.
Common uses
Callbacks
A library function takes a function pointer as a parameter and calls it as needed. The C standard library’s qsort is the canonical example:
int compare_int(const void *a, const void *b) {
return *(int *)a - *(int *)b;
}
int data[] = {5, 2, 8, 1, 9, 3};
qsort(data, 6, sizeof(int), compare_int);qsort doesn’t know how to compare your data — it can’t, because it doesn’t know your type. So you provide a comparison function and pass it in. The same qsort works for sorting strings (pass strcmp), structs (pass your custom comparator), or anything.
Dispatch tables
Map an enum or integer to a function instead of writing a switch:
typedef void (*Handler)(int, char *);
Handler handlers[N_TYPES] = {
[TYPE_HEADER] = handle_header,
[TYPE_PAYLOAD] = handle_payload,
[TYPE_FOOTER] = handle_footer,
};
void process(Packet *p) {
handlers[p->type](p->id, p->data);
}Adding a new packet type is one new entry, not a new switch case. Hot paths can avoid branch mispredictions because the indirect call is data-driven rather than control-flow-driven.
Polymorphism (vtables)
Object-oriented languages implement virtual methods using arrays of function pointers (“vtables”) attached to each object’s type. Calling obj->method() becomes “look up method in obj’s vtable, call the function at that slot.” C++ generates these automatically; you can build them by hand in C:
typedef struct Shape Shape;
typedef struct {
double (*area)(const Shape *);
double (*perimeter)(const Shape *);
} ShapeVTable;
struct Shape {
const ShapeVTable *vtable;
/* shape-specific data */
};
double get_area(const Shape *s) { return s->vtable->area(s); }Each concrete shape (Circle, Rectangle, etc.) provides its own vtable with its area / perimeter functions. get_area(some_shape) calls the right one based on the vtable pointer.
Plugin systems and FFI
Loading a shared library at runtime (dlopen / LoadLibrary) returns the addresses of exported functions — you then store those in function pointers and call through them. The host program doesn’t know at compile time what functions exist.
Type compatibility
Function pointer assignments are strictly type-checked:
int sum(int a, int b);
double dsum(double a, double b);
int (*op)(int, int);
op = sum; // ok — same signature
op = dsum; // compile error — return type and parameter types differ
op = compare_int; // compile error — different signatureCasting between incompatible function pointer types is undefined behaviour by the C standard, even if it “works” on a particular platform. Storing function pointers as void * is similarly non-portable (some embedded architectures have separate code and data address spaces, making the cast meaningless). The standard void (*)(void) is the safest “generic function pointer” type for storage.
NULL function pointers
A function pointer can be NULL — declared but not yet pointing at a function. Calling through a NULL function pointer is undefined behaviour, typically a crash. Always initialise function pointers (or check for NULL before calling):
typedef void (*Callback)(void);
Callback on_done = NULL;
/* ... */
if (on_done) on_done(); // skip if not registeredOptional callbacks (some events have handlers, others don’t) are the standard reason you’d intentionally leave a function pointer NULL.
Performance
Indirect calls through function pointers are slightly slower than direct calls — the CPU has to load the address before jumping. Modern CPUs predict indirect-jump targets well when the call site sees the same target repeatedly, but mispredicted jumps stall the pipeline.
For tight loops calling the same function through a pointer, the JIT/branch predictor handles it well. For polymorphic call sites that bounce between many targets, the indirection cost can be measurable — but rarely matters compared to whatever logic the call is implementing.
In other languages
Function pointers are a low-level building block. Most higher-level languages wrap them:
- C++ — pointers to free functions and pointers to member functions; lambdas with captures (closures);
std::function(type-erased callable). - Java / C# — interfaces and delegates;
Function<T,R>/Actiontypes. - Python — every function is a first-class object; pass and store functions like any other variable.
- JavaScript — functions are objects; passing functions as arguments is everywhere (event handlers, promises, callbacks).
Underneath, the implementation almost always uses function pointers — the language just hides the syntax.
In context
Function pointers are a C-language feature distinct from data pointers and from Dynamic memory allocation; they fit alongside Pointer arithmetic and C struct as the core indirection tools in C. Their primary use cases — callbacks, dispatch tables, polymorphism — show up in essentially every non-trivial C codebase.