Phase 3 · Structured & Modular CModule 14~44 min read

Function Pointers & Callbacks

Treat behavior as data with function pointers, callback interfaces, dispatch tables, and standard-library algorithms.

What you'll learn

Function pointers let a program choose behavior at runtime without hard-coding every operation. They power standard-library comparators, event handlers, dispatch tables, plugins, and reusable data-structure APIs.

By the end, you'll be able to:

  • Declare, assign, and call compatible function pointers
  • Pass callbacks into reusable algorithms
  • Sort generic arrays safely with qsort
  • Carry caller state through an explicit context pointer
Inversion of control with a callback

Caller

chooses behavior

passes →
callback

function address

invoked by →

Reusable algorithm

controls when

Function pointer syntax

In int (*operation)(int, int), parentheses makeoperation a pointer to a function accepting two ints and returning int. Without them, the declaration would mean a function returning an int pointer.

function_pointer.c
#include <stdio.h>

int add(int left, int right) { return left + right; }
int multiply(int left, int right) { return left * right; }

int main(void) {
    int (*operation)(int, int) = add;
    printf("%d\n", operation(3, 4));

    operation = multiply;
    printf("%d\n", operation(3, 4));
    return 0;
}

Note

A function name converts to a function pointer in this context. Bothoperation = add and operation = &add work; the shorter form is conventional.

Callbacks

A callback is a function supplied to another function. The reusable function controls when to invoke it; the caller controls what behavior runs.

for_each.c
#include <stddef.h>
#include <stdio.h>

void for_each(int values[], size_t count, void (*action)(int)) {
    for (size_t i = 0; i < count; i++) {
        action(values[i]);
    }
}

void print_square(int value) {
    printf("%d ", value * value);
}

int main(void) {
    int values[] = {1, 2, 3, 4};
    for_each(values, 4, print_square);
    putchar('\n');
    return 0;
}

Key idea

The callback signature is a contract. Return type and every parameter type must be compatible; calling through an incompatible function pointer is undefined behavior.

Sort with qsort

The standard qsort works with any element type. It receives an array base, element count, element size, and comparator. The comparator receives pointers to two elements and returns negative, zero, or positive.

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

int compare_ints(const void *left, const void *right) {
    int a = *(const int *) left;
    int b = *(const int *) right;
    return (a > b) - (a < b); // avoids subtraction overflow
}

int main(void) {
    int values[] = {40, 10, 30, 20};
    size_t count = sizeof values / sizeof values[0];

    qsort(values, count, sizeof values[0], compare_ints);
    for (size_t i = 0; i < count; i++) printf("%d ", values[i]);
    putchar('\n');
    return 0;
}

Do not compare with a - b

Subtraction can overflow and violate the comparator contract. Relational comparison such as(a > b) - (a < b) is safe for all int values.

Readable callback types

Complex pointer declarations become readable when named with typedef. A callback type also gives related APIs one consistent signature.

predicate.c
#include <stdbool.h>
#include <stddef.h>

typedef bool (*Predicate)(int value);

size_t count_if(const int values[], size_t count, Predicate matches) {
    size_t matches_count = 0;
    for (size_t i = 0; i < count; i++) {
        if (matches(values[i])) matches_count++;
    }
    return matches_count;
}
DeclarationMeaning
bool test(int)A function
bool (*test)(int)A variable pointing to such a function
typedef bool (*Predicate)(int)A reusable pointer-type alias

Dispatch tables

An array of function pointers maps an index or state to behavior. This replaces a large switch when every operation has one signature.

dispatch.c
#include <stdio.h>

typedef void (*Command)(void);

void create(void) { puts("create"); }
void save(void) { puts("save"); }
void quit(void) { puts("quit"); }

int main(void) {
    Command commands[] = {create, save, quit};
    enum { CREATE, SAVE, QUIT };

    int selected = SAVE;
    if (selected >= CREATE && selected <= QUIT) {
        commands[selected]();
    }
    return 0;
}

Watch out

Validate the index before calling through a table. An out-of-range access can produce an invalid address and undefined behavior.

Context pointers

C has no closures that automatically capture local state. Callback APIs often accept an additional void * context, which the caller owns and the callback converts back to the documented type.

context.c
#include <stddef.h>
#include <stdio.h>

typedef void (*Visitor)(int value, void *context);

void visit(const int values[], size_t count, Visitor visitor, void *context) {
    for (size_t i = 0; i < count; i++) {
        visitor(values[i], context);
    }
}

void add_to_total(int value, void *context) {
    int *total = context;
    *total += value;
}

int main(void) {
    int values[] = {5, 10, 15};
    int total = 0;
    visit(values, 3, add_to_total, &total);
    printf("%d\n", total);
    return 0;
}

Tip

The context pointer separates reusable callback logic from global variables. Document its type, nullability, mutability, ownership, and required lifetime.

Interface safety

  • Check nullable callback pointers before calling
  • Use one exact signature and a typedef for related callbacks
  • Never cast an incompatible function just to silence the compiler
  • Keep callback and context lifetimes valid for the entire registration or call
  • Document whether re-entry or concurrent invocation is possible

Recap & quick check

Key takeaways

  • A function pointer stores the address of a function with a compatible signature.
  • Callbacks separate an algorithm's control flow from caller-selected behavior.
  • qsort comparators return negative, zero, or positive and must avoid overflow.
  • typedef makes callback contracts readable and consistent.
  • A void context pointer carries explicit caller state without relying on globals.

Quick check

1. Why are parentheses required in int (*operation)(int)?

2. What must a qsort comparator return for equal elements?

3. What problem does a context pointer solve?

4. Is casting between incompatible function-pointer signatures safe?

Phase 3 complete. Phase 4 begins with Module 15 — Bitwise Programming & Data Representation, where C works directly with bits and byte layouts.