↓ Skip to main content

6 Advanced Applications of C Function Pointers

6 Advanced Applications of C Function Pointers

Function pointers are one of the most powerful mechanisms in the C programming language. At a basic level, they allow a program to store the address of a function and invoke it indirectly.

Their real value, however, becomes apparent when function pointers are used to separate behavior from control flow. This enables callbacks, configurable algorithms, dispatch tables, state machines, and object-oriented design patterns without requiring language-level support for interfaces or virtual methods.

This article explores six practical applications of C function pointers:

  1. Callback functions
  2. Function parameterization
  3. Sorting with custom comparators
  4. Function-pointer arrays and dispatch tables
  5. Backtracking with callbacks
  6. Polymorphism using structures and function pointers

The examples use standard C and focus on patterns that appear frequently in systems programming, embedded software, libraries, and performance-sensitive applications.

🔔 1. Callback Functions
#

A callback is a function supplied to another function so that it can be invoked later when a particular event or condition occurs.

This pattern separates the mechanism that detects an event from the code that handles it.

#include <stdio.h>

typedef void (*event_callback_t)(void);

void handle_event(int event_type, event_callback_t callback)
{
    printf("event %d occurred\n", event_type);

    if (callback != NULL) {
        callback();
    }
}

void callback_function(void)
{
    printf("callback function called\n");
}

int main(void)
{
    handle_event(1, callback_function);
    handle_event(2, NULL);

    return 0;
}

handle_event() does not need to know what the callback actually does. It only needs to know that the function matches the expected signature.

This pattern is common in:

  • GUI frameworks
  • Device drivers
  • Interrupt-related software
  • Networking stacks
  • Event-driven systems
  • Embedded firmware

Callback design considerations
#

A callback interface should define its function signature carefully. In production C code, explicitly using void for functions that take no arguments is preferable to an empty parameter list because it provides a precise prototype.

For more complex APIs, callbacks are often paired with a user-defined context pointer:

typedef void (*event_callback_t)(int event, void *context);

This allows the same callback implementation to operate on different application objects or state without relying on global variables.

🧩 2. Function Parameterization
#

Function pointers can be used to parameterize behavior.

Instead of writing separate functions for every possible transformation, a generic function can receive the desired operation as a function pointer.

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

typedef int (*transform_func_t)(int);

void process_array(int *array, size_t size, transform_func_t process)
{
    for (size_t i = 0; i < size; ++i) {
        array[i] = process(array[i]);
    }
}

int increment(int value)
{
    return value + 1;
}

int square(int value)
{
    return value * value;
}

int main(void)
{
    int array[] = {1, 2, 3, 4, 5};
    size_t size = sizeof(array) / sizeof(array[0]);

    process_array(array, size, increment);

    for (size_t i = 0; i < size; ++i) {
        printf("%d ", array[i]);
    }

    printf("\n");

    return 0;
}

The important design principle is that process_array() owns the iteration mechanism, while the callback owns the element-level behavior.

The same function can therefore support operations such as:

  • Incrementing values
  • Squaring values
  • Negating values
  • Clamping values
  • Applying calibration functions
  • Performing application-specific transformations

This is a simple form of higher-order programming implemented using C’s function-pointer mechanism.

🔢 3. Sorting with Comparison Functions
#

A classic use of function pointers is passing a comparison function to a generic sorting algorithm.

The C standard library’s qsort() uses exactly this approach.

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

typedef int (*compare_func_t)(const void *, const void *);

void sort(int *array, size_t size, compare_func_t compare)
{
    qsort(array, size, sizeof(array[0]), compare);
}

int compare_int(const void *a, const void *b)
{
    int lhs = *(const int *)a;
    int rhs = *(const int *)b;

    return (lhs > rhs) - (lhs < rhs);
}

int compare_reverse_int(const void *a, const void *b)
{
    int lhs = *(const int *)a;
    int rhs = *(const int *)b;

    return (rhs > lhs) - (rhs < lhs);
}

int main(void)
{
    int array[] = {3, 1, 4, 1, 5, 9};
    size_t size = sizeof(array) / sizeof(array[0]);

    sort(array, size, compare_int);

    for (size_t i = 0; i < size; ++i) {
        printf("%d ", array[i]);
    }

    printf("\n");

    return 0;
}

The comparator determines the ordering while the sorting algorithm remains independent of the actual data relationship.

Avoiding comparator overflow
#

A common beginner implementation is:

return *(const int *)a - *(const int *)b;

Although concise, this can overflow for sufficiently large integer values.

For example, subtracting a large negative value from a large positive value can exceed the range of int, producing undefined behavior.

The expression:

return (lhs > rhs) - (lhs < rhs);

avoids that problem while producing the required negative, zero, or positive result.

This is a small but important detail when writing production-quality C comparators.

🗂️ 4. Function-Pointer Arrays and Dispatch Tables
#

An array of function pointers can act as a dispatch table.

Instead of writing a long sequence of if/else statements or a large switch, the program can map an operation identifier directly to a function.

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

typedef void (*operation_func_t)(int, int);

void add(int a, int b)
{
    printf("%d + %d = %d\n", a, b, a + b);
}

void subtract(int a, int b)
{
    printf("%d - %d = %d\n", a, b, a - b);
}

void multiply(int a, int b)
{
    printf("%d * %d = %d\n", a, b, a * b);
}

void divide(int a, int b)
{
    if (b != 0) {
        printf("%d / %d = %d\n", a, b, a / b);
    } else {
        printf("cannot divide by zero\n");
    }
}

int main(void)
{
    operation_func_t operations[] = {
        add,
        subtract,
        multiply,
        divide
    };

    int a = 10;
    int b = 5;

    for (size_t i = 0; i < sizeof(operations) / sizeof(operations[0]); ++i) {
        operations[i](a, b);
    }

    return 0;
}

The table provides a direct mapping between an index and an operation.

This pattern is especially useful for:

  • Command interpreters
  • Protocol handlers
  • Finite-state machines
  • Opcode dispatch
  • Device drivers
  • Menu systems
  • Embedded command processors

Dispatch tables in embedded systems
#

Dispatch tables are particularly attractive in firmware because they can replace complicated control-flow structures with a compact table-driven design.

For example:

typedef void (*command_handler_t)(void);

static const command_handler_t command_table[] = {
    handle_start,
    handle_stop,
    handle_reset,
    handle_status
};

A received command ID can then select the corresponding handler directly.

Bounds checking remains essential:

if (command_id < COMMAND_COUNT) {
    command_table[command_id]();
}

Without validation, an invalid index can turn a function-pointer table into an arbitrary control-flow vulnerability.

🔄 5. Backtracking with Function Pointers
#

Function pointers are also useful in recursive algorithms.

A backtracking algorithm can focus exclusively on generating candidate solutions while delegating the handling of completed solutions to a callback.

The following example generates permutations:

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

typedef void (*permutation_callback_t)(const int *, size_t);

static void swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void permute(
    int *numbers,
    size_t length,
    size_t depth,
    permutation_callback_t callback)
{
    if (depth == length) {
        callback(numbers, length);
        return;
    }

    for (size_t i = depth; i < length; ++i) {
        swap(&numbers[depth], &numbers[i]);

        permute(numbers, length, depth + 1, callback);

        swap(&numbers[depth], &numbers[i]);
    }
}

void print_array(const int *array, size_t length)
{
    for (size_t i = 0; i < length; ++i) {
        printf("%d ", array[i]);
    }

    printf("\n");
}

int main(void)
{
    int numbers[] = {1, 2, 3};

    permute(
        numbers,
        sizeof(numbers) / sizeof(numbers[0]),
        0,
        print_array
    );

    return 0;
}

The permutation algorithm does not need to know what should happen when a solution is found.

The callback could instead:

  • Count solutions.
  • Store solutions.
  • Filter solutions.
  • Compare solutions against constraints.
  • Send solutions to another subsystem.

This separation makes the recursive algorithm reusable without modifying its core traversal logic.

🧱 6. Implementing Polymorphism in C
#

C does not provide classes, inheritance, or virtual methods. However, structures combined with function pointers can implement many of the same behavioral patterns.

A common technique is to place a function pointer in a base structure and embed that structure as the first member of derived structures.

#include <stdio.h>

typedef struct shape shape_t;

struct shape {
    void (*draw)(shape_t *);
};

typedef struct {
    shape_t shape;
    int x;
    int y;
    int radius;
} circle_t;

typedef struct {
    shape_t shape;
    int x;
    int y;
    int width;
    int height;
} rectangle_t;

void circle_draw(shape_t *shape)
{
    circle_t *circle = (circle_t *)shape;

    printf(
        "Drawing a circle at (%d, %d) with radius %d.\n",
        circle->x,
        circle->y,
        circle->radius
    );
}

void rectangle_draw(shape_t *shape)
{
    rectangle_t *rectangle = (rectangle_t *)shape;

    printf(
        "Drawing a rectangle at (%d, %d) with width %d and height %d.\n",
        rectangle->x,
        rectangle->y,
        rectangle->width,
        rectangle->height
    );
}

int main(void)
{
    circle_t circle = {
        .shape = {circle_draw},
        .x = 10,
        .y = 20,
        .radius = 5
    };

    rectangle_t rectangle = {
        .shape = {rectangle_draw},
        .x = 30,
        .y = 40,
        .width = 15,
        .height = 20
    };

    shape_t *shapes[] = {
        (shape_t *)&circle,
        (shape_t *)&rectangle
    };

    for (size_t i = 0; i < 2; ++i) {
        shapes[i]->draw(shapes[i]);
    }

    return 0;
}

The caller interacts with both objects through the common shape_t interface:

shapes[i]->draw(shapes[i]);

The actual function invoked depends on the object’s function pointer.

Conceptually, this is similar to a virtual method call in an object-oriented language.

Function pointers as virtual interfaces
#

This pattern is widely used in C-based:

  • GUI frameworks
  • Embedded systems
  • Device drivers
  • Networking stacks
  • Game engines
  • Hardware abstraction layers
  • Operating-system components

A more scalable implementation can place several operations into a structure, effectively creating a vtable-like interface:

typedef struct {
    int  (*open)(void *context);
    int  (*read)(void *context, void *buffer, size_t size);
    int  (*write)(void *context, const void *buffer, size_t size);
    void (*close)(void *context);
} device_ops_t;

Different hardware implementations can then provide different operation tables while sharing the same high-level interface.

This is one of the most important design patterns behind portable C interfaces.

🧠 Conclusion
#

C function pointers are much more than a mechanism for indirectly calling a function. They provide a lightweight way to separate interfaces from implementations and behavior from control flow.

The six patterns covered here demonstrate their versatility:

  • Callbacks enable event-driven and asynchronous designs.
  • Behavior parameterization makes algorithms reusable.
  • Comparison functions allow generic sorting and ordering.
  • Dispatch tables provide efficient runtime operation selection.
  • Backtracking callbacks separate search logic from result handling.
  • Function-pointer interfaces provide polymorphic behavior without C++-style objects.

The same fundamental mechanism appears throughout operating systems, embedded firmware, device drivers, networking stacks, standard libraries, and other systems-level software.

Once function pointers are combined with structures, context pointers, and well-defined interfaces, C can support surprisingly sophisticated abstractions while retaining explicit control over memory, execution, and data representation.

Related