C function pointers callbacks qsort polymorphism guide
|

C Function Pointers: Callbacks, Sorting, and Runtime Polymorphism

What Are Function Pointers?

In C, functions live in memory just like variables. A function pointer is a variable that stores the address of a function. You can pass functions as arguments to other functions, store them in arrays, and call them indirectly at runtime. This is how C achieves flexibility without classes or inheritance.

If you’ve ever used qsort(), you’ve already used a function pointer — that comparison function you pass in? That’s a callback, and callbacks are function pointers in action.

Before diving in, make sure you’re solid on C Pointers and C Functions. Function pointers combine both concepts.

Syntax Breakdown

Function pointer syntax is notoriously confusing the first time you see it. Let’s break it down piece by piece.

// A normal function:
int add(int a, int b) {
    return a + b;
}

// A pointer to that function:
int (*fp)(int, int);

// Assign the function's address:
fp = add;        // &add also works, but the & is optional

// Call through the pointer:
int result = fp(3, 4);   // result = 7
// (*fp)(3, 4) also works, but fp(3, 4) is cleaner

Let’s decode int (*fp)(int, int):

  • int — the return type of the function being pointed to
  • (*fp)fp is a pointer (the parentheses are critical!)
  • (int, int) — the function takes two int parameters

Why the parentheses matter: Without them, int *fp(int, int) declares a function named fp that returns int*. The parentheses around *fp force the compiler to parse it as a pointer.

// Here's a complete example showing declaration, assignment, and invocation:
#include <stdio.h>

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

int main(void) {
    // Declare a function pointer
    int (*operation)(int, int);

    // Point to multiply
    operation = multiply;
    printf("5 * 3 = %d\n", operation(5, 3));  // 15

    // Reassign to subtract
    operation = subtract;
    printf("5 - 3 = %d\n", operation(5, 3));  // 2

    return 0;
}

Callback Functions

A callback is a function you pass to another function so it can be called back at the right time. This is the most common use of function pointers. It lets you write generic code that works with any operation.

#include <stdio.h>

// Generic: applies any operation to each element of an array
void apply_to_array(int *arr, int size, int (*transform)(int)) {
    for (int i = 0; i < size; i++) {
        arr[i] = transform(arr[i]);
    }
}

int double_it(int x) { return x * 2; }
int square_it(int x) { return x * x; }
int negate_it(int x) { return -x; }

void print_array(int *arr, int size) {
    for (int i = 0; i < size; i++) printf("%d ", arr[i]);
    printf("\n");
}

int main(void) {
    int nums[] = {1, 2, 3, 4, 5};
    int n = 5;

    printf("Original: ");
    print_array(nums, n);

    apply_to_array(nums, n, double_it);
    printf("Doubled:  ");
    print_array(nums, n);  // 2 4 6 8 10

    apply_to_array(nums, n, square_it);
    printf("Squared:  ");
    print_array(nums, n);  // 4 16 36 64 100

    apply_to_array(nums, n, negate_it);
    printf("Negated:  ");
    print_array(nums, n);  // -4 -16 -36 -64 -100

    return 0;
}

apply_to_array doesn’t know or care what transform does. It just calls it. You can pass any function that takes an int and returns an int. This is the same pattern that makes qsort() work with any data type.

qsort: The Standard Library’s Power Tool

qsort() from <stdlib.h> is a generic sorting function that uses a callback for comparisons. It’s the single most practical example of function pointers in C. If you understand C Arrays, you already know the data side — now we add the function pointer side.

Sorting Integers

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

int compare_ints_asc(const void *a, const void *b) {
    int ia = *(const int *)a;
    int ib = *(const int *)b;
    return (ia > ib) - (ia < ib);  // Safe comparison (no overflow)
}

int compare_ints_desc(const void *a, const void *b) {
    return compare_ints_asc(b, a);  // Just swap arguments
}

int main(void) {
    int nums[] = {42, 7, 99, 1, 55, 23};
    int n = sizeof(nums) / sizeof(nums[0]);

    qsort(nums, n, sizeof(int), compare_ints_asc);
    printf("Ascending:  ");
    for (int i = 0; i < n; i++) printf("%d ", nums[i]);
    printf("\n");  // 1 7 23 42 55 99

    qsort(nums, n, sizeof(int), compare_ints_desc);
    printf("Descending: ");
    for (int i = 0; i < n; i++) printf("%d ", nums[i]);
    printf("\n");  // 99 55 42 23 7 1

    return 0;
}

Why (ia > ib) - (ia < ib) instead of ia - ib? Because ia - ib can overflow. If ia is INT_MAX and ib is -1, the subtraction wraps around to a negative number, giving a wrong result. The comparison expression is always safe.

Sorting Strings

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

int compare_strings(const void *a, const void *b) {
    // qsort passes pointers to the array elements
    // Array elements are char*, so we receive char**
    const char *sa = *(const char **)a;
    const char *sb = *(const char **)b;
    return strcmp(sa, sb);
}

int main(void) {
    const char *languages[] = {"Rust", "C", "Python", "Go", "JavaScript", "Zig"};
    int n = sizeof(languages) / sizeof(languages[0]);

    qsort(languages, n, sizeof(char *), compare_strings);

    for (int i = 0; i < n; i++) printf("%s\n", languages[i]);
    // C, Go, JavaScript, Python, Rust, Zig

    return 0;
}

Sorting Structs

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

typedef struct {
    char name[50];
    int score;
} Player;

int compare_by_score(const void *a, const void *b) {
    const Player *pa = (const Player *)a;
    const Player *pb = (const Player *)b;
    return (pb->score > pa->score) - (pb->score < pa->score);  // Descending
}

int compare_by_name(const void *a, const void *b) {
    const Player *pa = (const Player *)a;
    const Player *pb = (const Player *)b;
    return strcmp(pa->name, pb->name);
}

int main(void) {
    Player players[] = {
        {"Alice", 950}, {"Bob", 1200}, {"Charlie", 800},
        {"Diana", 1100}, {"Eve", 750}
    };
    int n = sizeof(players) / sizeof(players[0]);

    qsort(players, n, sizeof(Player), compare_by_score);
    printf("By score (descending):\n");
    for (int i = 0; i < n; i++)
        printf("  %s: %d\n", players[i].name, players[i].score);

    qsort(players, n, sizeof(Player), compare_by_name);
    printf("\nBy name (alphabetical):\n");
    for (int i = 0; i < n; i++)
        printf("  %s: %d\n", players[i].name, players[i].score);

    return 0;
}

The power here is that qsort handles the sorting algorithm. You only define how to compare. One generic algorithm, infinite comparison strategies.

Function Pointer Arrays

You can store function pointers in arrays, enabling dispatch tables — a technique used in interpreters, calculators, and state machines.

Calculator Dispatch Table

#include <stdio.h>

double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double divide(double a, double b) {
    if (b == 0) { fprintf(stderr, "Division by zero!\n"); return 0; }
    return a / b;
}

int main(void) {
    // Array of function pointers
    double (*operations[])(double, double) = { add, sub, mul, divide };
    const char *symbols[] = { "+", "-", "*", "/" };

    double a = 10.0, b = 3.0;

    for (int i = 0; i < 4; i++) {
        printf("%.1f %s %.1f = %.2f\n", a, symbols[i], b, operations[i](a, b));
    }
    // 10.0 + 3.0 = 13.00
    // 10.0 - 3.0 = 7.00
    // 10.0 * 3.0 = 30.00
    // 10.0 / 3.0 = 3.33

    return 0;
}

State Machine

Function pointer arrays are perfect for state machines, where the current state determines which function runs next:

#include <stdio.h>

typedef enum { STATE_IDLE, STATE_RUNNING, STATE_PAUSED, STATE_STOPPED, NUM_STATES } State;

State handle_idle(void)    { printf("IDLE: Starting...\n");    return STATE_RUNNING; }
State handle_running(void) { printf("RUNNING: Pausing...\n");  return STATE_PAUSED; }
State handle_paused(void)  { printf("PAUSED: Resuming...\n");  return STATE_RUNNING; }
State handle_stopped(void) { printf("STOPPED: Done.\n");       return STATE_STOPPED; }

int main(void) {
    State (*handlers[])(void) = {
        [STATE_IDLE]    = handle_idle,
        [STATE_RUNNING] = handle_running,
        [STATE_PAUSED]  = handle_paused,
        [STATE_STOPPED] = handle_stopped,
    };

    State current = STATE_IDLE;
    int steps = 0;

    while (current != STATE_STOPPED && steps < 6) {
        current = handlers[current]();
        steps++;
    }

    return 0;
}

This eliminates long switch statements. Adding a new state is just adding a function and an entry in the array. The designated initializer syntax ([STATE_IDLE] = handle_idle) makes the mapping crystal clear.

Typedef for Simplification

Function pointer syntax gets ugly fast. typedef saves your sanity:

// Without typedef:
int (*operation)(int, int);
void apply(int *arr, int n, int (*func)(int));
int (*ops[])(int, int) = { add, sub, mul };

// With typedef:
typedef int (*BinaryOp)(int, int);
typedef int (*UnaryOp)(int);

BinaryOp operation;
void apply(int *arr, int n, UnaryOp func);
BinaryOp ops[] = { add, sub, mul };

The typedef version reads like English. BinaryOp is a type that means “pointer to a function taking two ints and returning an int.” Use typedef for any function pointer you use more than once.

Runtime Polymorphism with Struct VTables

C doesn’t have classes or virtual methods, but you can build the same thing with structs and function pointers. This is how the Linux kernel implements its device drivers, file systems, and network protocols.

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

// "Interface" via function pointers
typedef struct Shape Shape;

struct Shape {
    double (*area)(Shape *self);
    double (*perimeter)(Shape *self);
    void (*describe)(Shape *self);
    void (*destroy)(Shape *self);
};

// --- Circle ---
typedef struct {
    Shape base;      // Must be first member for casting
    double radius;
} Circle;

double circle_area(Shape *self) {
    Circle *c = (Circle *)self;
    return M_PI * c->radius * c->radius;
}

double circle_perimeter(Shape *self) {
    Circle *c = (Circle *)self;
    return 2.0 * M_PI * c->radius;
}

void circle_describe(Shape *self) {
    Circle *c = (Circle *)self;
    printf("Circle(radius=%.1f)\n", c->radius);
}

void circle_destroy(Shape *self) {
    free(self);
}

Shape *circle_create(double radius) {
    Circle *c = malloc(sizeof(Circle));
    if (!c) { perror("malloc"); exit(1); }
    c->base.area = circle_area;
    c->base.perimeter = circle_perimeter;
    c->base.describe = circle_describe;
    c->base.destroy = circle_destroy;
    c->radius = radius;
    return (Shape *)c;
}

// --- Rectangle ---
typedef struct {
    Shape base;
    double width, height;
} Rectangle;

double rect_area(Shape *self) {
    Rectangle *r = (Rectangle *)self;
    return r->width * r->height;
}

double rect_perimeter(Shape *self) {
    Rectangle *r = (Rectangle *)self;
    return 2.0 * (r->width + r->height);
}

void rect_describe(Shape *self) {
    Rectangle *r = (Rectangle *)self;
    printf("Rectangle(%.1f x %.1f)\n", r->width, r->height);
}

void rect_destroy(Shape *self) {
    free(self);
}

Shape *rect_create(double width, double height) {
    Rectangle *r = malloc(sizeof(Rectangle));
    if (!r) { perror("malloc"); exit(1); }
    r->base.area = rect_area;
    r->base.perimeter = rect_perimeter;
    r->base.describe = rect_describe;
    r->base.destroy = rect_destroy;
    r->width = width;
    r->height = height;
    return (Shape *)r;
}

// --- Generic code that works with any shape ---
void print_shape_info(Shape *s) {
    s->describe(s);
    printf("  Area:      %.2f\n", s->area(s));
    printf("  Perimeter: %.2f\n", s->perimeter(s));
}

int main(void) {
    Shape *shapes[] = {
        circle_create(5.0),
        rect_create(4.0, 6.0),
        circle_create(2.5),
        rect_create(10.0, 3.0),
    };
    int n = sizeof(shapes) / sizeof(shapes[0]);

    for (int i = 0; i < n; i++) {
        print_shape_info(shapes[i]);
        printf("\n");
    }

    // Clean up
    for (int i = 0; i < n; i++) {
        shapes[i]->destroy(shapes[i]);
    }

    return 0;
}

This is real-world C. The print_shape_info function doesn’t know whether it’s dealing with a Circle or Rectangle. It calls s->area(s), and the right function runs based on what was stored at creation time. This is exactly how C++ virtual methods work under the hood. If you want to understand the memory layout, revisit C Structs.

Building an Event System

Event-driven programming relies on function pointers. Here’s a minimal event system where you register handlers for named events:

#include <stdio.h>
#include <string.h>

#define MAX_HANDLERS 16
#define MAX_EVENTS 8

typedef void (*EventHandler)(const char *event, void *data);

typedef struct {
    char event_name[32];
    EventHandler handlers[MAX_HANDLERS];
    int handler_count;
} EventSlot;

typedef struct {
    EventSlot slots[MAX_EVENTS];
    int slot_count;
} EventSystem;

void event_init(EventSystem *es) {
    es->slot_count = 0;
}

static EventSlot *find_slot(EventSystem *es, const char *event) {
    for (int i = 0; i < es->slot_count; i++) {
        if (strcmp(es->slots[i].event_name, event) == 0)
            return &es->slots[i];
    }
    return NULL;
}

void event_on(EventSystem *es, const char *event, EventHandler handler) {
    EventSlot *slot = find_slot(es, event);
    if (!slot) {
        if (es->slot_count >= MAX_EVENTS) {
            fprintf(stderr, "Too many event types!\n");
            return;
        }
        slot = &es->slots[es->slot_count++];
        strncpy(slot->event_name, event, sizeof(slot->event_name) - 1);
        slot->event_name[sizeof(slot->event_name) - 1] = '\0';
        slot->handler_count = 0;
    }
    if (slot->handler_count < MAX_HANDLERS) {
        slot->handlers[slot->handler_count++] = handler;
    }
}

void event_emit(EventSystem *es, const char *event, void *data) {
    EventSlot *slot = find_slot(es, event);
    if (!slot) return;
    for (int i = 0; i < slot->handler_count; i++) {
        slot->handlers[i](event, data);
    }
}

// --- Example handlers ---
void on_login(const char *event, void *data) {
    printf("[AUTH] User logged in: %s\n", (char *)data);
}

void on_login_log(const char *event, void *data) {
    printf("[LOG] Login event recorded for: %s\n", (char *)data);
}

void on_error(const char *event, void *data) {
    printf("[ERROR] %s\n", (char *)data);
}

int main(void) {
    EventSystem es;
    event_init(&es);

    // Register multiple handlers for the same event
    event_on(&es, "login", on_login);
    event_on(&es, "login", on_login_log);
    event_on(&es, "error", on_error);

    // Emit events
    event_emit(&es, "login", "admin");
    event_emit(&es, "error", "Connection timeout");
    event_emit(&es, "login", "guest");

    return 0;
}

This pattern is the foundation of GUI frameworks, game engines, and server architectures. The event system doesn’t know what handlers do — it just stores and calls them. Adding new behavior means registering a new function, not modifying existing code.

Common Patterns: forEach and filter

Function pointers let you write generic iteration and filtering functions, similar to higher-order functions in other languages:

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

// forEach: apply a function to each element
void for_each(int *arr, int n, void (*action)(int, int)) {
    for (int i = 0; i < n; i++) {
        action(arr[i], i);
    }
}

// filter: return a new array of elements matching a predicate
int *filter(int *arr, int n, int (*predicate)(int), int *out_size) {
    int *result = malloc(n * sizeof(int));
    if (!result) return NULL;

    int count = 0;
    for (int i = 0; i < n; i++) {
        if (predicate(arr[i])) {
            result[count++] = arr[i];
        }
    }

    *out_size = count;
    return result;
}

// Callbacks
void print_element(int val, int idx) {
    printf("  [%d] = %d\n", idx, val);
}

int is_even(int x) { return x % 2 == 0; }
int is_positive(int x) { return x > 0; }

int main(void) {
    int data[] = {-3, 7, 2, -1, 8, 0, 5, -4, 6};
    int n = sizeof(data) / sizeof(data[0]);

    printf("All elements:\n");
    for_each(data, n, print_element);

    int size;
    int *evens = filter(data, n, is_even, &size);
    printf("\nEven numbers (%d found):\n", size);
    for_each(evens, size, print_element);
    free(evens);

    int *positives = filter(data, n, is_positive, &size);
    printf("\nPositive numbers (%d found):\n", size);
    for_each(positives, size, print_element);
    free(positives);

    return 0;
}

This is functional programming in C. The filter function uses C Dynamic Memory Allocation because we don’t know the result size until we scan the input. The caller must free() the returned array.

Common Mistakes

1. Missing Parentheses in Declaration

int *fp(int, int);        // WRONG: declares a function returning int*
int (*fp)(int, int);      // RIGHT: declares a pointer to a function

This is the #1 beginner mistake with function pointers. The parentheses around *fp are not optional.

2. Calling a NULL Function Pointer

void (*callback)(void) = NULL;
callback();  // SEGFAULT!

Always check for NULL before calling: if (callback) callback();

3. Wrong Function Signature

int add(int a, int b) { return a + b; }
void (*fp)(int) = (void (*)(int))add;  // Compiles with cast, but UNDEFINED BEHAVIOR

The function pointer type must match the function’s actual signature. Casting to silence a warning doesn’t fix the problem — it hides it. Calling a function through an incompatible pointer type is undefined behavior.

4. Confusing Function Pointer Syntax with Arrays

// Array of function pointers:
int (*ops[4])(int, int);

// Pointer to an array of functions (rare, but different):
int (*(*ops_ptr)[4])(int, int);

When in doubt, use typedef to make the type obvious.

5. Forgetting void* Casts in qsort Callbacks

The comparator receives const void* pointers. You must cast them to the correct type before dereferencing. Forgetting the cast or casting to the wrong type leads to garbage values or crashes.

Practice Exercises

  1. Generic map function: Write int *map(int *arr, int n, int (*f)(int)) that returns a new array where each element is f(arr[i]). Test with functions that double, triple, and absolute-value the elements.
  2. Sort anything: Create a struct Student with name, gpa, and age fields. Write comparison functions to sort by each field, then use qsort to sort an array of students three different ways.
  3. Command dispatcher: Build a command-line calculator that reads operations like "add 3 5", "mul 2 7", etc. Use a function pointer array indexed by command name (via a hash or string comparison) to dispatch to the right operation.
  4. Extend the vtable pattern: Add a Triangle shape to the polymorphism example. Include fields for all three sides and implement the area using Heron’s formula.
  5. Generic reduce/fold: Write int reduce(int *arr, int n, int init, int (*combine)(int, int)) that folds an array into a single value. Test with sum, product, and max.
  6. Timer callback system: Build a system where you register functions with a delay (in milliseconds). A tick() function decrements all timers and fires callbacks whose timers reach zero. Simulate time in a loop.

Function pointers unlock a level of flexibility in C that many programmers never explore. They’re the key to writing reusable, modular, and extensible C code. For more on how functions and their parameters work together, see C Function Parameters & Return. Ready for the full path? Check the C Programming Roadmap.

Similar Posts

Leave a Reply

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