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.

They’re how callbacks, dispatch tables, plugin systems, and polymorphism work in C, and underneath the abstractions in C++, Python, and most other languages.

Declaring and using

A function pointer’s type encodes the function’s signature: return type and parameter types. The syntax mimics a function declaration but wraps the pointer name in parentheses:

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 function-pointer noise disappears from the variable declarations. Arrays of function pointers 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. 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

Distinct from data pointers and from Dynamic memory allocation; they sit alongside Pointer arithmetic and C struct as the core indirection tools in C.