If you have ever wondered how qsort accepts any comparison logic, how operating systems implement interrupt handlers, or how game engines wire up AI behaviours at runtime without recompiling — function pointers are the answer. They are one of the most powerful, elegant, and often feared features in C and C++.
In this deep-dive guide, we demystify function pointers from first principles: how they exist in memory, how to declare and invoke them, how to use them for callbacks and dispatch tables, and how they evolve into modern C++ constructs like std::function and lambdas — with annotated diagrams and complete code examples throughout.
🧠 What Exactly Is a Function Pointer?
In C and C++, every function you write is compiled into a sequence of machine instructions that sit at a specific address in memory — exactly like any other data. A function pointer is a variable that stores that address, allowing you to call the function indirectly, pass it as an argument, store it in a struct, or swap it at runtime.
This is not magic. It is the same pointer arithmetic you already use with data — except the thing being pointed at is executable code rather than a byte of data.
📖 Declaring Function Pointers — Syntax Dissected
The syntax for function pointers trips up nearly every developer the first time. The trick is to read from the inside out, following the “clockwise/spiral” rule.
The Parentheses Are Non-Negotiable
Writing int *fp(int, int) (no parens around *fp) means something completely different: a function named fp that returns int*. Always wrap the pointer with parentheses: int (*fp)(int, int).
Syntax Reference Table
| Declaration | What It Means | Matches Function Like |
|---|---|---|
void (*fp)(void) | Pointer to a no-arg void function | void greet(void) |
int (*fp)(int, int) | Pointer to function taking two ints, returning int | int add(int a, int b) |
double (*fp)(double) | Pointer to function taking double, returning double | double sqrt(double) |
char* (*fp)(const char*, int) | Pointer to function returning char pointer | char* substr(const char*, int) |
int (*fp[4])(void) | Array of 4 function pointers | 4 functions matching int f(void) |
⚙️ Your First Function Pointer — Hello, Indirection
#include <stdio.h>
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int sub(int a, int b) { return a - b; }
int main(void) {
/* Declare a function pointer matching signature (int, int) -> int */
int (*operation)(int, int);
/* Assign — &add and add are equivalent; both give the address */
operation = add;
printf("add(3,4) = %d\n", operation(3, 4)); /* → 7 */
/* Reassign to a different function at runtime */
operation = mul;
printf("mul(3,4) = %d\n", operation(3, 4)); /* → 12 */
operation = sub;
printf("sub(3,4) = %d\n", operation(3, 4)); /* → -1 */
/* Explicit dereference — both styles are valid C */
printf("(*operation)(3,4) = %d\n", (*operation)(3, 4));
return 0;
}
Two Equivalent Calling Styles
Both fp(a, b) and (*fp)(a, b) are legal C. The explicit dereference form makes it visually clear you are calling through a pointer.
🏷️ Cleaning Up Syntax with typedef and using
Raw function pointer declarations become unreadable fast. typedef in C and using in C++ create named types that restore clarity.
/* ——— C style — typedef ——— */
typedef int (*BinaryOp)(int, int);
BinaryOp add_ptr = add;
BinaryOp ops[3] = { add, mul, sub };
int apply(BinaryOp op, int x, int y) {
return op(x, y);
}
/* ——— C++11 style — using alias ——— */
using BinaryOp = int(*)(int, int);
/* Without typedef — hard to read */
void register_handler(int event, void(*handler)(int, const char*));
/* With typedef — intent is immediately clear */
typedef void (*EventHandler)(int event_code, const char* payload);
void register_handler(int event, EventHandler handler);
📚 Callbacks — Passing Behaviour as an Argument
The most common real-world use of function pointers is the callback pattern: a library function does general work, and you supply the specialised logic. The C standard library’s qsort is the textbook example.
#include <stdio.h>
#include <stdlib.h>
/* qsort signature:
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
*/
int cmp_int_asc(const void* a, const void* b) {
int ia = *(const int*)a;
int ib = *(const int*)b;
return (ia > ib) - (ia < ib);
}
int cmp_int_desc(const void* a, const void* b) {
return cmp_int_asc(b, a);
}
typedef struct { char name[32]; int score; } Player;
int cmp_by_score(const void* a, const void* b) {
const Player* pa = (const Player*)a;
const Player* pb = (const Player*)b;
return pb->score - pa->score;
}
int main(void) {
int nums[] = { 5, 2, 8, 1, 9, 3 };
size_t n = sizeof(nums) / sizeof(nums[0]);
qsort(nums, n, sizeof(int), cmp_int_asc);
/* nums: 1 2 3 5 8 9 */
qsort(nums, n, sizeof(int), cmp_int_desc);
/* nums: 9 8 5 3 2 1 */
Player leaderboard[] = {
{ "Alice", 9800 }, { "Bob", 12400 }, { "Carol", 7200 }
};
qsort(leaderboard, 3, sizeof(Player), cmp_by_score);
/* → Bob 12400, Alice 9800, Carol 7200 */
return 0;
}
🔧 Dispatch Tables — O(1) Function Selection
Instead of a long if-else or switch chain, store function pointers in an array indexed by a key. The runtime cost becomes a single array lookup — O(1) regardless of how many operations you have.
❌ Naive switch — O(n) branching
- Grows linearly with new operations
- Hard to extend without modifying the switch
- Hard to load from config or plugins
✅ Dispatch table — O(1) lookup
- Array index = function, constant time
- New operations: add a row to the table
- Foundation for virtual method tables (vtables)
typedef double (*MathOp)(double, double);
double op_add(double a, double b) { return a + b; }
double op_sub(double a, double b) { return a - b; }
double op_mul(double a, double b) { return a * b; }
double op_div(double a, double b) { return (b != 0.0) ? a / b : 0.0; }
typedef struct {
char symbol;
MathOp handler;
const char* name;
} OpEntry;
static const OpEntry DISPATCH_TABLE[] = {
{ '+', op_add, "add" },
{ '-', op_sub, "subtract" },
{ '*', op_mul, "multiply" },
{ '/', op_div, "divide" },
};
MathOp find_op(char sym) {
for (size_t i = 0; i < sizeof(DISPATCH_TABLE)/sizeof(DISPATCH_TABLE[0]); i++)
if (DISPATCH_TABLE[i].symbol == sym)
return DISPATCH_TABLE[i].handler;
return NULL; /* always check for NULL before calling! */
}
int main(void) {
char op = '*'; double a = 6.0, b = 7.0;
MathOp fn = find_op(op);
if (fn)
printf("%.1f %c %.1f = %.1f\n", a, op, b, fn(a, b)); /* 42.0 */
return 0;
}
🎯 State Machines — Runtime Behaviour Switching
Function pointers make state machines elegant: the current state is stored as a function pointer itself. Each state function processes events and returns the next state.
#include <stdio.h>
typedef struct StateFn StateFn;
typedef StateFn (*StateFunc)(char event);
StateFn state_idle(char ev);
StateFn state_running(char ev);
StateFn state_error(char ev);
struct StateFn { StateFunc fn; const char* name; };
StateFn state_idle(char ev) {
printf("[IDLE] event='%c' → ", ev);
if (ev == 's') { printf("→ RUNNING\n"); return (StateFn){state_running, "running"}; }
printf("stay IDLE\n");
return (StateFn){state_idle, "idle"};
}
StateFn state_running(char ev) {
printf("[RUNNING] event='%c' → ", ev);
if (ev == 'p') { printf("→ IDLE\n"); return (StateFn){state_idle, "idle"}; }
if (ev == 'e') { printf("→ ERROR\n"); return (StateFn){state_error, "error"}; }
printf("stay RUNNING\n");
return (StateFn){state_running, "running"};
}
StateFn state_error(char ev) {
printf("[ERROR] → reset → IDLE\n");
return (StateFn){state_idle, "idle"};
}
int main(void) {
StateFn current = {state_idle, "idle"};
char events[] = { 's', 'x', 'e', 'r', 's' };
for (int i = 0; i < 5; i++)
current = current.fn(events[i]);
return 0;
}
🏛️ Function Pointers in Structs — Manual Polymorphism
C++ virtual functions are implemented using a hidden pointer to a vtable — an array of function pointers. You can build the same mechanism manually in C, which is exactly what the Linux kernel, CPython, and SQLite do internally.
#include <stdio.h>
#include <stdlib.h>
typedef struct Shape Shape;
typedef struct {
double (*area) (const Shape* self);
double (*perimeter)(const Shape* self);
void (*describe) (const Shape* self);
void (*destroy) (Shape* self);
} ShapeVTable;
struct Shape {
const ShapeVTable* vtable;
};
typedef struct {
Shape base; /* MUST be first */
double radius;
} Circle;
static double circle_area(const Shape* s) {
return 3.14159265 * ((const Circle*)s)->radius * ((const Circle*)s)->radius;
}
static void circle_describe(const Shape* s) {
printf("Circle r=%.2f area=%.2f\n", ((const Circle*)s)->radius, circle_area(s));
}
static void circle_destroy(Shape* s) { free(s); }
static const ShapeVTable CIRCLE_VTABLE = {
circle_area, NULL, circle_describe, circle_destroy
};
Shape* circle_new(double r) {
Circle* c = malloc(sizeof(Circle));
c->base.vtable = &CIRCLE_VTABLE;
c->radius = r;
return (Shape*)c;
}
void shape_describe_all(Shape* shapes[], int n) {
for (int i = 0; i < n; i++)
shapes[i]->vtable->describe(shapes[i]); /* virtual dispatch! */
}
int main(void) {
Shape* shapes[] = { circle_new(5.0), circle_new(3.5) };
shape_describe_all(shapes, 2);
for (int i = 0; i < 2; i++) shapes[i]->vtable->destroy(shapes[i]);
return 0;
}
🌎 Real-World Use Cases
Plugin Systems
Load .so / .dll at runtime with dlopen + dlsym, cast the returned void* to a function pointer, and call into plugin code without recompiling.
Signal Handlers
POSIX signal(SIGINT, handler) registers a function pointer the OS calls when the signal fires. The kernel itself stores and invokes your pointer.
Game AI Behaviour
Store the current AI state as a function pointer. Swap it to change behaviour (patrol → chase → attack) without an enormous switch statement.
Event Systems
GUI frameworks store arrays of function pointers per event type. When a button is clicked, iterate the registered callbacks and invoke each one.
Linux Kernel (file_operations)
Every file system driver fills a struct file_operations with function pointers: read, write, open, ioctl. The VFS calls through them.
Database Engines
SQLite uses function pointers in its B-tree implementation for comparison functions, allowing it to sort keys of arbitrary type.
❌ Common Pitfalls and How to Avoid Them
if (fp != NULL) before calling. A NULL function pointer call is undefined behaviour — usually a segfault.
void(*)(int) to void(*)(double) and calling it is undefined behaviour. Types must match exactly.
dlsym). Use a union or cast through uintptr_t carefully.
__cdecl and __stdcall through a mismatched pointer corrupts the stack silently.
BinaryOp fp = NULL;
if (fn != NULL) fn(code); or provide a no-op default handler to avoid crashes.
🍒 C++ Evolution — std::function and Lambdas
Raw function pointers have two major limitations in C++: they cannot capture state (closures), and they do not work with member functions without ugly casting. C++11 introduced two powerful upgrades.
#include <functional>
#include <vector>
#include <algorithm>
#include <iostream>
int double_it(int x) { return x * 2; }
int main() {
// Style 1: Raw function pointer — zero overhead, no state capture
int (*fp)(int) = double_it;
std::cout << fp(5) << "\n"; // → 10
// Style 2: std::function — holds ANY callable
std::function<int(int)> fn = double_it;
fn = [](int x) { return x + 100; };
std::cout << fn(5) << "\n"; // → 105
// Style 3: Lambda with capture — captures local variables
int multiplier = 3;
auto scale = [multiplier](int x) { return x * multiplier; };
std::cout << scale(7) << "\n"; // → 21
// Style 4: Template parameter — zero overhead, inlined at compile time
auto apply = []<typename F>(F&& fn, int x) { return fn(x); };
std::cout << apply(double_it, 6); // → 12
return 0;
}
| Mechanism | Captures State? | Runtime Cost | Best Used For |
|---|---|---|---|
int (*fp)(int) | ❌ No | Zero — direct call | C interop, signal handlers, plugin ABI |
std::function<int(int)> | ✓ Yes | ~20–40 ns (may heap-alloc) | Storing mixed callables in containers |
Lambda auto f = [x]{…} | ✓ Yes | Zero if no std::function wrap | Inline callbacks, STL algorithms |
Template <typename F> | ✓ Yes | Zero — inlined at compile time | High-performance generic code |
The Rule of Thumb for C++
Prefer lambdas with template parameters for performance-critical callbacks. Use std::function only when you need to store heterogeneous callables in a container. Fall back to raw function pointers when interfacing with C APIs or defining a stable ABI.
🧩 Member Function Pointers in C++
class Calculator {
public:
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
};
int main() {
int (Calculator::*op)(int, int);
Calculator calc;
op = &Calculator::add;
std::cout << (calc.*op)(3, 4) << "\n"; // → 7
op = &Calculator::mul;
std::cout << (calc.*op)(3, 4) << "\n"; // → 12
// Modern C++ — wrap in a lambda for ergonomic use
auto bound = [&calc, op](int a, int b) { return (calc.*op)(a, b); };
std::cout << bound(4, 5) << "\n"; // → 20
return 0;
}
🔮 Where to Go Next
🎯 Conclusion
Function pointers are not an arcane C trick — they are the fundamental mechanism behind callbacks, dispatch tables, vtables, plugin systems, and the entire paradigm of runtime polymorphism.
- A function pointer is simply a variable holding the address of a function in the code segment
typedef/usingaliases make declarations human-readable and self-documenting- Dispatch tables use function pointers to achieve O(1) runtime selection
- Struct-based vtables in C implement interface polymorphism without a C++ compiler
- In C++, lambdas and
std::functionsupersede raw pointers for stateful, ergonomic callbacks - Always initialise to
NULLand guard before calling to prevent undefined behaviour
About MindWeave Engineering
We write in-depth technical guides for systems programmers, embedded engineers, and performance-focused developers. Our articles go beyond syntax — we explain the why beneath the code, with diagrams that make the invisible visible.

Comments