Lesson 06 / 15
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.
Published Updated
In this lesson
- 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
Contents
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
// 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 ints 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
// 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.
// 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.
// 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 (
tableinoperation_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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.