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 6

The 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 — op is 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 signature

Casting 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 registered

Optional 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> / Action types.
  • 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.