---
title: 'Function Pointers'
source: 'https://academia.sh/en/courses/c-memory/function-pointers'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Defines a function pointer and calls it to indirectly run another function'
    - 'Passes a function as a parameter to another function to set up a callback'
    - "Measures that ownership of a callback's carried context belongs to the caller"
---

# Function Pointers

Pointers that point at a function itself and can be passed as a parameter to another function; the ownership of a callback's context belongs to the caller.

The five lessons up to this point measured cases where a pointer points at **data** — an
integer, an array, an opaque record. This last lesson measures a pointer that points at
something different: **the function itself**. A function pointer holds the address of a
compiled function and lets that function be called indirectly — this is the foundation of
choosing a function at runtime, or of passing one to another function **as a parameter**.

A function pointer's declaration looks confusing at first glance: the parentheses in
`int (*op)(int, int)` are not arbitrary, they are required — had we written
`int *op(int, int)`, this would be the **declaration** of a function named `op` that
returns `int *`, not a pointer named `op`. The parentheses group `*op` as a name first and
tell the compiler "`op` is a pointer, and what it points at is a function matching this
signature." This complexity is usually hidden behind a `typedef` — indeed the
`void (*func)(int, void *)` parameter type in `callback_context.c` follows the same
pattern as the `BinaryOp` definition in `operation_table.c`; one just writes the type
directly, the other binds it to a name.

## Defining and Calling a Function Pointer

```c
// function_pointer.c — a function pointer holds the address of a compiled function
#include <stdio.h>

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

static int multiply(int a, int b) {
    return a * b;
}

int main(void) {
    int (*op)(int, int) = add;
    printf("%s %d\n", "indirect call through add", op(3, 4));

    op = multiply;
    printf("%s %d\n", "indirect call through multiply", op(3, 4));

    printf("%s %d\n", "is op equal to add", (int)(op == add));
    printf("%s %d\n", "is op equal to multiply", (int)(op == multiply));
    return 0;
}
```

```
indirect call through add 7
indirect call through multiply 12
is op equal to add 0
is op equal to multiply 1
```

**PO25.** The declaration `int (*op)(int, int)` says that `op` will hold the address of a
function that takes two `int`s and returns an `int`. After the assignment `op = add`, the
call `op(3, 4)` gives **7** — it indirectly runs `add`. `op` is then **reassigned** to
`multiply`, and the same call syntax (`op(3, 4)`) now gives **12**. `op == add` is **0**,
`op == multiply` is **1** — this shows function pointers can be compared with equality
just like data pointers; `op` matches whichever function it currently points at.

## Callback: Passing a Function to Another Function

```c
// callback.c — passing a function as a parameter chooses behavior at runtime
#include <stdio.h>

static int square(int x) {
    return x * x;
}

static int negate(int x) {
    return -x;
}

static void apply_and_print(int value, int (*transform)(int), const char *label) {
    printf("%s %d\n", label, transform(value));
}

int main(void) {
    apply_and_print(6, square, "square transform");
    apply_and_print(6, negate, "negate transform");
    return 0;
}
```

```
square transform 36
negate transform -6
```

**PO26.** The `apply_and_print` function, through its `int (*transform)(int)` parameter,
lets **the caller decide which function runs** — the names `square` and `negate` never
appear inside the function's own body. The first call passes `square` and the result is
**36** (6*6), the second passes `negate` and the result comes out **-6**. This pattern
achieves different behaviors without changing the same `apply_and_print` body — the
behavior is chosen **as a parameter**, at call time rather than compile time. This pattern
is called a **callback**: `transform` calls back, from inside `apply_and_print`, into the
function the caller supplied.

What differs from the `add`/`multiply` example is that the function itself is no longer
**held** in a variable, it flows directly as a parameter. The `op` variable was persistent
inside `main` and could be reassigned; `transform`, on the other hand, is a temporary
parameter that only exists while `apply_and_print` is running. Both uses share the same
function-pointer mechanism, but one demonstrates **holding state** (which function `op`
currently points at), the other **one-shot dispatch** (which function to use for this
call).

## Ownership of Context: The Data Carried to a Callback

A callback often carries not just a value, but a **context** that accompanies that value.
Who should own this context? The following example measures the case where the caller
supplies a context and manages that context's lifetime itself.

```c
// callback_context.c — the owner of the context carried to a callback is the caller that supplied it
#include <stdio.h>

typedef struct {
    int factor;
    int sum;
} Context;

static void accumulate(int value, void *context) {
    Context *ctx = context;
    ctx->sum += value * ctx->factor;
}

static void run_over_array(const int *array, int length,
                            void (*func)(int, void *), void *context) {
    for (int i = 0; i < length; i++) {
        func(array[i], context);
    }
}

int main(void) {
    int values[4] = {1, 2, 3, 4};
    Context ctx = {.factor = 3, .sum = 0};

    run_over_array(values, 4, accumulate, &ctx);
    printf("%s %d\n", "sum with factor", ctx.sum);

    ctx.factor = 1;
    ctx.sum = 0;
    run_over_array(values, 4, accumulate, &ctx);
    printf("%s %d\n", "sum without factor", ctx.sum);
    return 0;
}
```

```
sum with factor 30
sum without factor 10
```

**PO27.** The `Context` struct is defined as a **local variable** inside `main` — it is
never allocated with `count_alloc` or `malloc`, its class is **language**. But it is passed
as `&ctx` to `run_over_array`, and that function **forwards** it to `accumulate` through
the `void *context` parameter. `accumulate` does not take ownership of `context` itself —
it only writes into memory that `main` owns. On the first run the factor is **3** and the
sum is **30** (3*(1+2+3+4)); on the second run the factor is reset to **1** and the sum
comes out **10** — `ctx` can be reused because its lifetime still belongs to `main`. The
same rule would hold even if the context passed to a callback had been dynamically
allocated: the party **supplying** the context (the caller) would remain obligated to free
it, a callback function like `accumulate` never takes ownership of it itself.

This pattern shares the same ownership shape as this course's first lesson's opaque record
observation, but from the opposite direction: there, the **callee** (`owner.c`) held its
own data and only lent it to the caller; here, the **caller** holds its own data, and the
**callee** (`accumulate`, `run_over_array`) only accesses that data through a pointer. In
both cases the data stays with a single side; what changes is only which side holds the
memory. The `void *context` parameter here follows the same rule as the `void *` in the
void and opaque pointers lesson (05): the `accumulate` function first converts `context`
to the `Context *` type, and only then accesses its fields — the type information is
carried by the caller, the callback function only recovers it.

## An Array of Function Pointers: A Dispatch Table

Just as a function pointer can be reassigned on its own, it can also be held inside an
**array** — this makes it possible to select which function runs at runtime through an
index.

```c
// operation_table.c — an array of function pointers builds a runtime dispatch table
#include <stdio.h>

typedef int (*BinaryOp)(int, int);

static int add(int a, int b) { return a + b; }
static int subtract(int a, int b) { return a - b; }
static int multiply(int a, int b) { return a * b; }

int main(void) {
    BinaryOp table[3] = { add, subtract, multiply };
    const char *names[3] = { "add", "subtract", "multiply" };
    int results[3];

    for (int i = 0; i < 3; i++) {
        results[i] = table[i](10, 4);
        printf("%s %d\n", names[i], results[i]);
    }

    int total = 0;
    for (int i = 0; i < 3; i++) {
        total += results[i];
    }
    printf("%s %d\n", "sum of results", total);
    return 0;
}
```

```
add 14
subtract 6
multiply 40
sum of results 60
```

This table is a dispatch form that replaces an `if`/`else if` chain or a `switch`
statement: adding a new operation is limited to adding an element to the `table` and
`names` arrays, the calling code itself does not change.

**PO28.** Each element of the `table` array holds the address of a different function; the
call `table[i](10, 4)` selects which function runs **at runtime, based on the index
`i`** — there is no need to write three separate lines like `add(10, 4)`, `subtract(10, 4)`,
`multiply(10, 4)` in the source code. The `results` array, like `table` itself, is defined
as a local variable inside `main`; both have class **caller** — `main` allocates them,
fills and reads them, and once the function calls are done they go out of scope and are
returned automatically. This is the same ownership shape as `Context ctx` in
`callback_context.c`: both the array and the struct are managed by the automatic lifetime
of the **language** class, but the array's *contents* (which functions sit in which order)
are entirely `main`'s decision — `add`, `subtract`, and `multiply` have no say in that
decision, they only do their own job when called.

## The Bounding Measurement

A function pointer's signature (like `int (*)(int, int)`) only matches parameter and
return types — it says nothing about **what** the function it points at does or **what
its side effects** are. `add` and `multiply` above share the same signature but do
completely different things; a function taking a callback parameter gets no compile-time
guarantee at all about the behavior of the function passed to it. This is another form of
the same limit found in earlier lessons — that pointer types do not carry ownership
information: a type guarantees **shape**, not **meaning**.

This limit turns into a separate risk when combined with the context carried to a
callback. In `callback_context.c`, at the moment the `run_over_array` call passes to
`accumulate`, `ctx` is still within `main`'s scope, so the memory `context` points at is
valid. But if a callback is registered to run **later** — say, in an event queue, after the
calling function has already returned — the address passed as context may by then have
outlived the automatic lifetime of the **language** class — the same dangling pointer
condition from lesson 04, here in callback form. This block is deliberately never run: a
function would write the address of a local `Context` variable into a callback record and
return, then when the registered callback fires, `context` would be pointing at a call
frame whose scope has already closed — dereferencing it is undefined behavior. In the
`run_over_array` example this risk does not exist, because the callback runs while still
inside `main`'s own call frame, while `ctx` is still alive; the risk only appears when the
context's lifetime and the callback's execution moment **diverge**. This is why the
obligation of the party supplying the context (the caller) is not limited to freeing the
memory — it must also guarantee the context stays alive until whenever the callback might
fire.

This pattern's most common use is in the C standard library: `qsort` requires an
`int (*)(const void *, const void *)` comparison function to sort an array, `bsearch` uses
the same signature for searching. `qsort` itself does not **know** which type it is
sorting — the same indirection pattern as the `table[i]` call in `operation_table.c`, but
this time the function being indirected is a comparison decision supplied by the caller.
The comparison function passed to `qsort` also has to first convert its own parameters to
the correct type — just like `accumulate` converting its `void *context` to `Context *`
first; the `const void *` in the signature does not carry type information here either, it
only stores it. The difference between the dispatch-table pattern and the `qsort` pattern
is only scale: one chooses among three fixed functions, the other lets the caller supply
**a different comparison logic each time** — both rest on the same foundation: carrying a
function like data and calling it indirectly when needed.

## The Ownership Axis Across This Topic: Four Classes, One Outcome

The Pointers topic's six lessons asked the same question in six different pointer forms:
**who owns this memory?** In lessons 01 and 02 the answer was **language** — local
variables and arrays inside `main` were returned automatically once scope closed, no side
called `free`. In lesson 03 ownership stayed with **language** again, but the problem was
less about ownership and more about **information loss**: an array lost its length when it
decayed to a pointer, which required `largest`, `matrix_decay.c`, and
`returns_length_too.c` to fill in the same missing information through three different
channels. In lesson 04 ownership split into three sources (language, caller, callee), and
the shared result was a dangling pointer — a value that keeps pointing at a block that has
already been freed. In lesson 05 ownership moved for the first time to a **hidden** class:
the opaque type was a contract where the **callee** held its own data and only lent it to
the caller. In this last lesson ownership returned to the **caller** again — but this time
what is at stake is not data, but a **function choice** and the context accompanying it;
the `Context ctx` in `callback_context.c` and the `table`/`results` arrays in
`operation_table.c` were memory that `main` allocated and `main` reclaimed.

What the four classes (language, caller, callee, unowned) have in common is that none of
them ever showed up in the pointer's **type** — neither `int *`, nor `Record *`, nor
`int (*)(int, int)` said which side was obligated to free it. This information always had
to be carried **from outside**, either through a naming convention (`_create`/`_destroy`
pairs), a comment, or a written contract. The next topic, Dynamic Memory, applies these
same four classes to the `malloc`/`free` pair itself; the classification here will not
change, only which functions represent which class will become clearer.

## Summary

- A function pointer (`int (*)(int, int)`) holds the address of a compiled function and
  lets that function be called indirectly; it can be reassigned to different functions.
- Passing a function as a parameter to another function sets up a **callback**; the
  called function's behavior changes at runtime depending on which function is passed.
- The context carried to a callback is usually the address of a local variable of the
  **language** class; the callback function does not **own** this context, it only uses
  it.
- The party supplying the context is the **caller**, and even if the context had been
  dynamically allocated, the obligation to free it would still remain with the caller.
- A function pointer's signature only matches parameter and return types; it does not
  guarantee the behavior of the function it points at.
- An array of function pointers (`table` in `operation_table.c`) builds a dispatch table
  selected by index at runtime; `qsort`/`bsearch`'s comparison parameter uses the same
  pattern in the standard library.

## Next Step

The Pointers topic closes here: the four classes (language, caller, callee, unowned) were
measured across six lessons, through both data pointers and function pointers. In every
lesson, what determined the class was never the pointer's type but which side took on
allocation and freeing — this distinction never showed up directly in the source code, it
was only ever conveyed through naming and contract. The next topic, Dynamic Memory, carries
these same four classes into the behavior of `malloc`, `calloc`, `realloc`, and `free`
themselves — failed allocation, the risks carried by resizing, and the discipline of
freeing.
