Function Pointers in C – Callbacks, Dispatch Tables, and qsort

Function pointers in C hold the address of a function and allow calling it indirectly — through the pointer rather than by name. This enables callbacks, dispatch tables, plugin architectures, and strategy patterns that are otherwise impossible in C without conditional branches. Every event-driven framework, interrupt vector table, and generic algorithm in embedded and systems C uses function pointers.

Syntax

/* A pointer to a function that takes two ints and returns int */
int (*compare)(int, int);

/* Calling through the pointer — both forms are equivalent */
result = compare(a, b);
result = (*compare)(a, b);   /* explicit dereference — also valid */

The parentheses around *compare are required — without them, int *compare(int, int) is a function that returns int *, not a pointer to a function.

typedef Cleans Up the Syntax

/* Without typedef — verbose */
int (*add)(int, int);
int (*subtract)(int, int);

/* With typedef — readable */
typedef int (*BinaryOp)(int, int);

BinaryOp add;
BinaryOp subtract;

The typedef names the type (BinaryOp) rather than a specific variable. Prefer typedef for function pointer types used in multiple places.

Function Pointer Program in C

#include <stdio.h>

/* ── Basic usage ─────────────────────────────────────────── */
typedef int (*BinaryOp)(int, int);

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

/* ── Passing a function pointer as an argument (callback) ── */
void apply_and_print(int x, int y, BinaryOp op, const char *name)
{
    printf("%s(%d, %d) = %d\n", name, x, y, op(x, y));
}

/* ── Array of function pointers (dispatch table) ─────────── */
static BinaryOp ops[]  = { add, subtract, multiply };
static const char *names[] = { "add", "subtract", "multiply" };

/* ── Generic sort comparator (like qsort) ─────────────────── */
void bubble_sort(int arr[], int n, int (*cmp)(int, int))
{
    int i, j, tmp, swapped;
    for (i = 0; i < n - 1; i++) {
        swapped = 0;
        for (j = 0; j < n - 1 - i; j++) {
            if (cmp(arr[j], arr[j+1]) > 0) {
                tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp;
                swapped = 1;
            }
        }
        if (!swapped) break;
    }
}

int cmp_asc (int a, int b) { return a - b; }   /* ascending  */
int cmp_desc(int a, int b) { return b - a; }   /* descending */

/* ── State machine using function pointers ────────────────── */
typedef void (*StateFunc)(void);

void state_idle   (void) { printf("State: IDLE\n");    }
void state_running(void) { printf("State: RUNNING\n"); }
void state_done   (void) { printf("State: DONE\n");    }

int main(void)
{
    int i, arr[] = {5, 2, 8, 1, 9, 3};
    int n = (int)(sizeof(arr) / sizeof(arr[0]));

    /* Assign and call through pointer */
    BinaryOp op = add;
    printf("Direct call:  add(10, 3) = %d\n", op(10, 3));

    op = multiply;
    printf("Repointed:    multiply(10, 3) = %d\n\n", op(10, 3));

    /* Callback */
    apply_and_print(10, 3, add,      "add");
    apply_and_print(10, 3, subtract, "subtract");
    apply_and_print(10, 3, multiply, "multiply");
    printf("\n");

    /* Dispatch table */
    printf("Dispatch table:\n");
    for (i = 0; i < 3; i++)
        printf("  ops[%d](%d, %d) = %d\n", i, 10, 4, ops[i](10, 4));
    printf("\n");

    /* Generic sort */
    bubble_sort(arr, n, cmp_asc);
    printf("Ascending:  ");
    for (i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    bubble_sort(arr, n, cmp_desc);
    printf("Descending: ");
    for (i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n\n");

    /* State machine */
    StateFunc states[] = { state_idle, state_running, state_done };
    for (i = 0; i < 3; i++)
        states[i]();   /* call through dispatch table */

    return 0;
}

How to Compile and Run

gcc -ansi -Wall -Wextra -o funcptr funcptr.c
./funcptr

Sample Output

Direct call:  add(10, 3) = 13
Repointed:    multiply(10, 3) = 30

add(10, 3) = 13
subtract(10, 3) = 7
multiply(10, 3) = 30

Dispatch table:
  ops[0](10, 4) = 14
  ops[1](10, 4) = 6
  ops[2](10, 4) = 40

Ascending:  1 2 3 5 8 9
Descending: 9 8 5 3 2 1

State: IDLE
State: RUNNING
State: DONE

How the Standard Library Uses Function Pointers

The C standard library qsort function is the canonical example:

#include <stdlib.h>

void qsort(void *base, size_t n, size_t size,
           int (*compar)(const void *, const void *));

The compar function pointer is what makes qsort generic — it works for any data type by delegating the comparison decision to the caller:

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

int cmp_int(const void *a, const void *b)
{
    return (*(int *)a) - (*(int *)b);
}

int main(void)
{
    int arr[] = {5, 2, 8, 1, 9, 3};
    int i, n = (int)(sizeof(arr) / sizeof(arr[0]));

    qsort(arr, (size_t)n, sizeof(int), cmp_int);

    for (i = 0; i < n; i++)
        printf("%d ", arr[i]);
    printf("\n");
    return 0;
}

Function Pointers in Embedded C

Interrupt vector tables are arrays of function pointers, defined at a hardware-specified memory address:

/* Conceptual interrupt vector table */
typedef void (*ISR)(void);

void default_handler(void) { /* do nothing */ }
void uart_irq(void)         { /* handle UART data */ }
void timer_irq(void)        { /* handle timer tick */ }

/* Vector table — address is MCU-specific */
ISR vector_table[] = {
    default_handler,   /* NMI */
    default_handler,   /* HardFault */
    timer_irq,         /* TIM2 */
    uart_irq,          /* USART1 */
};

HAL (Hardware Abstraction Layer) libraries like STM32’s also use function pointers extensively as callbacks for DMA completion, I2C events, and error conditions — the same pattern as the apply_and_print callback above.

Common Mistakes

  • Missing parentheses: int *fp(int) is a function returning int *; int (*fp)(int) is a pointer to a function. The parentheses are not optional.
  • Calling a NULL function pointer: always check if (fp != NULL) before calling a callback that may not have been registered.
  • Mismatched signature: calling through a function pointer whose signature doesn’t match the actual function’s signature is undefined behaviour. The typedef approach prevents this — the compiler catches mismatches when you assign.
  • Comparing function pointers to non-NULL values: only equality with NULL is portable. Treating function pointers as integer addresses is implementation-defined.

What This Teaches

  • Indirection through pointers applies to code, not just data: a function pointer is exactly like a data pointer — it stores an address, can be passed, stored in arrays, and reassigned
  • Callbacks are just function pointers with a contract: the caller provides storage for the function pointer; the callee invokes it — this is the core pattern behind every event system
  • Dispatch tables eliminate switch statements: an array of function pointers indexed by an integer state or opcode is often cleaner, faster, and more extensible than a switch/case chain

Related Topics


As an Amazon Associate we earn from qualifying purchases.

Recommended Book

Function pointers and the qsort/bsearch pattern are covered in section 5.11 of The C Programming Language by Kernighan & Ritchie. The table-driven lookup pattern from section 6.6 is the direct ancestor of every modern dispatch table. Also on Amazon.com.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>