The C Path — learn C, visually

🎯 Pointers & Memory

Function pointers: code is data too

⏱ 13 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

When you click a button and the right code runs, or a game lets you rebind keys to actions, something stored which function to call in a variable. That's a function pointer — the one kind of pointer you haven't met yet, aimed at code instead of data. It's also the trick that lets C's standard sort routine sort values of any type you invent, and you'll teach it to do exactly that before this lesson ends.

Here's a thought: functions live in memory too (the .text segment of the memory map — the part that holds your compiled code, remember?). So they have addresses. So… a pointer can hold one. A function pointer lets you store "which function to call" in a variable, pass it around, and decide at runtime what code runs. This one idea powers plugins, "run this when the user clicks" machinery, and the standard library's ability to sort anything.

The syntax, decoded

int (*op)(int, int); — read from the name outward: op is a pointer (*op), to a function taking (int, int), returning int. The parentheses around *op are mandatory: without them, int *op(int, int) declares a function returning int* — a completely different creature.

fnptr.c
#include <stdio.h>

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }

int main(void) {
    int (*op)(int, int);      /* op: ptr to int(int,int)  */

    op = add;                 /* function name -> address */
    printf("op(3, 4) = %d\n", op(3, 4));

    op = mul;                 /* retarget at runtime      */
    printf("op(3, 4) = %d\n", op(3, 4));
    return 0;
}
terminal
$ gcc fnptr.c -o fnptr && ./fnptr
op(3, 4) = 7
op(3, 4) = 12
# same call site, different code executed — decided at runtime

Two conveniences to notice: a bare function name decays to its address (no & needed, though &add also works), and you call through the pointer with plain op(3, 4) (though (*op)(3, 4) also works). C offers both spellings; modern style uses the short ones.

🧠 Checkpoint: What does int (*f)(int); declare?

  • A function returning int *
  • A pointer to a function taking an int and returning an int
  • An int pointer named f(int)
  • Nothing — it’s a syntax error
Show answer

A pointer to a function taking an int and returning an int — The parens force *f to bind first: f is a pointer, to a function (int), returning int. Drop them — int *f(int); — and you’ve declared a function returning int* instead.

Callbacks: teaching qsort to compare

The standard library's qsort can sort an array of anything — because you hand it a function pointer that knows how to compare two elements. It sorts; you judge. That's a callback:

sortme.c
#include <stdio.h>
#include <stdlib.h>

int by_value(const void *pa, const void *pb) {
    int a = *(const int *)pa;      /* cast back to real type */
    int b = *(const int *)pb;
    return (a > b) - (a < b);      /* -1 / 0 / 1, no overflow */
}

int main(void) {
    int a[] = { 42, 7, 19, 3, 25 };
    size_t n = sizeof a / sizeof a[0];

    qsort(a, n, sizeof a[0], by_value);  /* <- the callback */

    for (size_t i = 0; i < n; i++)
        printf("%d ", a[i]);
    printf("\n");
    return 0;
}
terminal
$ gcc sortme.c -o sortme && ./sortme
3 7 19 25 42

The comparator receives const void * pointers — generic addresses, because qsort has no idea what type it's sorting. Your first job inside is always to cast back to the real type.

⚠️

Resist return a - b; in int comparators. If a is huge and b is hugely negative, the subtraction overflows — undefined behavior, and real-world sorting bugs. The idiom (a > b) - (a < b) yields a clean −1 / 0 / 1 with no overflow, ever.

🧠 Checkpoint: Why does a qsort comparator take const void * parameters?

  • void* comparisons are faster
  • qsort works with ANY element type, so it hands you generic addresses
  • It’s required for all function pointers
  • To prevent the comparator from being inlined
Show answer

qsort works with ANY element type, so it hands you generic addresses — qsort only knows "n elements of size bytes each". It passes raw addresses; your comparator supplies the type knowledge by casting. That’s generic programming, C style.

Dispatch tables: an array of behaviors

Since function pointers are values, you can put them in arrays — and suddenly a chain of if/else becomes a table lookup. This is the skeleton of interpreters, menu systems, and state machines:

dispatch.c
#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; }

int main(void) {
    int (*ops[3])(int, int) = { add, sub, mul };
    const char *names[3]    = { "add", "sub", "mul" };

    for (int i = 0; i < 3; i++)          /* pick behavior by  */
        printf("%s(10, 4) = %d\n",       /* INDEX, no if/else */
               names[i], ops[i](10, 4));
    return 0;
}
terminal
$ gcc dispatch.c -o dispatch && ./dispatch
add(10, 4) = 14
sub(10, 4) = 6
mul(10, 4) = 6
# opcode -> handler: this is how bytecode interpreters dispatch

🧠 Checkpoint: What is wrong with return a - b; in an int comparator?

  • The sign convention is backwards
  • Nothing — it’s the recommended idiom
  • The subtraction can overflow (e.g. INT_MAX − INT_MIN) — undefined behavior
  • Comparators must return exactly −1, 0, or 1
Show answer

The subtraction can overflow (e.g. INT_MAX − INT_MIN) — undefined behavior — qsort only needs the SIGN, and any negative/zero/positive value is fine — but a − b can overflow for extreme inputs, which is UB. (a > b) − (a < b) is safe for every pair of ints.

Taming the syntax with typedef

Function pointer types get ugly fast — so give them a name. One typedef and declarations become readable English:

typedef.c (fragment)
typedef int (*binop)(int, int);  /* name the TYPE once     */

binop op = add;                  /* suddenly readable       */
binop table[8];                  /* an array of callbacks   */

int apply(binop f, int a, int b) {
    return f(a, b);              /* higher-order C          */
}

🤔 Decipher this declaration: int (*calc[4])(double, double);

Boss fight

Read from the name, spiraling outward: calc … is an array of 4 ([4] binds before *) … pointers … to functions taking (double, double)returning int. A ready-made dispatch table! With a typedef it deflates to: typedef int (*cmp2)(double, double); cmp2 calc[4]; — which is why real codebases always typedef their function pointer types.

This spot has an interactive editor widget — open the interactive lesson to play with it.

🎉 That's Pointers & Memory conquered — you can now point at anything C has to offer. Next part, we slow down and master the type system itself: const, volatile, casts, and the dark art of undefined behavior.

▶ Practice this lesson interactively (with live gcc)