Part of C in 100s

C in 100 Seconds: Function Pointers | Episode 27

Celest KimCelest Kim

Video: C in 100 Seconds: Function Pointers — Callbacks and Dispatch | Episode 27 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

C Function Pointers: Callbacks and Dispatch

int (*fp)(int, int) = add;fp is 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.