3 min read
On this page

Function Pointers

What Is a Function Pointer?

A function pointer holds the address of a function. You can call the function through the pointer, pass it as an argument, or store it in a data structure.

#include <stdio.h>

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

int main(void) {
    int (*op)(int, int);  /* declare a function pointer */

    op = add;
    printf("add: %d\n", op(3, 2));

    op = subtract;
    printf("subtract: %d\n", op(3, 2));

    return 0;
}
add: 5
subtract: 1

The declaration int (*op)(int, int) means: op is a pointer to a function that takes two int parameters and returns an int. The parentheses around *op are required; without them, int *op(int, int) declares a function returning int*.

Using typedef for Readability

Function pointer declarations are hard to read. A typedef gives them a clean name:

typedef int (*BinaryOp)(int, int);

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

void apply_and_print(BinaryOp op, int a, int b) {
    printf("Result: %d\n", op(a, b));
}

int main(void) {
    apply_and_print(add, 5, 3);
    apply_and_print(multiply, 5, 3);
    return 0;
}
Result: 8
Result: 15

With the typedef, BinaryOp replaces the unwieldy int (*)(int, int) everywhere.

Callback Patterns

A callback is a function pointer passed to another function, which calls it at the right time. This is the most common use of function pointers.

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

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

struct Button {
    const char *label;
    EventHandler on_click;
    void *user_data;
};

void button_click(struct Button *btn) {
    if (btn->on_click) {
        btn->on_click("click", btn->user_data);
    }
}

void my_handler(const char *event, void *user_data) {
    const char *name = (const char *)user_data;
    printf("Button '%s' received event: %s\n", name, event);
}

int main(void) {
    struct Button btn = {
        .label = "Submit",
        .on_click = my_handler,
        .user_data = "Submit",
    };

    button_click(&btn);
    return 0;
}
Button 'Submit' received event: click

The void *user_data parameter lets the callback access context without global variables. This pattern appears in GUI frameworks, event loops, and asynchronous I/O libraries.

Function Pointer Arrays & Dispatch Tables

An array of function pointers creates a dispatch table, mapping an index to a function:

#include <stdio.h>

typedef double (*MathFunc)(double, double);

double op_add(double a, double b) { return a + b; }
double op_sub(double a, double b) { return a - b; }
double op_mul(double a, double b) { return a * b; }
double op_div(double a, double b) { return b != 0 ? a / b : 0; }

int main(void) {
    MathFunc dispatch[] = {op_add, op_sub, op_mul, op_div};
    const char *names[] = {"+", "-", "*", "/"};

    double a = 10.0, b = 3.0;

    for (int i = 0; i < 4; i++) {
        printf("%.1f %s %.1f = %.2f\n", a, names[i], b, dispatch[i](a, b));
    }

    return 0;
}
10.0 + 3.0 = 13.00
10.0 - 3.0 = 7.00
10.0 * 3.0 = 30.00
10.0 / 3.0 = 3.33

Dispatch tables replace long switch statements and make it easy to add new operations.

qsort: The Standard Library Callback

The qsort function sorts an array using a comparator function pointer:

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

int compare_ints_ascending(const void *a, const void *b) {
    int ia = *(const int *)a;
    int ib = *(const int *)b;
    return (ia > ib) - (ia < ib);
}

int compare_strings(const void *a, const void *b) {
    const char *sa = *(const char **)a;
    const char *sb = *(const char **)b;
    return strcmp(sa, sb);
}

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

    qsort(nums, n, sizeof(int), compare_ints_ascending);

    printf("Sorted ints: ");
    for (int i = 0; i < n; i++) printf("%d ", nums[i]);
    printf("\n");

    const char *words[] = {"banana", "apple", "cherry", "date"};
    int w = sizeof(words) / sizeof(words[0]);

    qsort(words, w, sizeof(const char *), compare_strings);

    printf("Sorted strings: ");
    for (int i = 0; i < w; i++) printf("%s ", words[i]);
    printf("\n");

    return 0;
}
Sorted ints: 1 7 13 42 99
Sorted strings: apple banana cherry date

The comparator returns a negative value if a < b, zero if a == b, and a positive value if a > b. Never return a - b for integers because it can overflow.

The Strategy Pattern in C

The strategy pattern selects an algorithm at runtime. In C, this is a struct containing function pointers:

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

struct Logger {
    void (*log)(const char *message, void *ctx);
    void *ctx;
};

void log_to_stdout(const char *message, void *ctx) {
    (void)ctx;
    printf("[LOG] %s\n", message);
}

void log_to_file(const char *message, void *ctx) {
    FILE *f = (FILE *)ctx;
    fprintf(f, "[LOG] %s\n", message);
}

void do_work(struct Logger *logger) {
    logger->log("Starting work", logger->ctx);
    /* ... actual work ... */
    logger->log("Work complete", logger->ctx);
}

int main(void) {
    struct Logger console_logger = {
        .log = log_to_stdout,
        .ctx = NULL,
    };

    do_work(&console_logger);

    FILE *f = fopen("app.log", "w");
    if (f) {
        struct Logger file_logger = {
            .log = log_to_file,
            .ctx = f,
        };
        do_work(&file_logger);
        fclose(f);
    }

    return 0;
}
[LOG] Starting work
[LOG] Work complete

This is how C achieves polymorphism: a struct of function pointers replaces a vtable.

Signal Handlers

The signal function registers a callback for operating system signals:

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

volatile sig_atomic_t running = 1;

void handle_sigint(int sig) {
    (void)sig;
    running = 0;
}

int main(void) {
    signal(SIGINT, handle_sigint);

    printf("Press Ctrl+C to stop...\n");
    while (running) {
        /* main loop */
    }
    printf("\nCaught SIGINT, shutting down.\n");
    return 0;
}

The signal function takes a function pointer void (*)(int) as its second argument. Signal handlers must be async-signal-safe, meaning they can only call a limited set of functions and should only modify volatile sig_atomic_t variables.

Virtual Dispatch at the C Level

Object-oriented virtual dispatch is just a struct of function pointers. This is how C implements polymorphism, and it is how C++ vtables work under the hood.

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

struct Shape;

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

struct Shape {
    const struct ShapeVtable *vtable;
};

/* Circle */
struct Circle {
    struct Shape base;
    double radius;
};

static double circle_area(const struct Shape *self) {
    const struct Circle *c = (const struct Circle *)self;
    return 3.14159265 * c->radius * c->radius;
}

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

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

static const struct ShapeVtable circle_vtable = {
    .area = circle_area,
    .describe = circle_describe,
    .destroy = circle_destroy,
};

struct Shape *circle_create(double radius) {
    struct Circle *c = malloc(sizeof(*c));
    if (!c) return NULL;
    c->base.vtable = &circle_vtable;
    c->radius = radius;
    return &c->base;
}

/* Rectangle */
struct Rect {
    struct Shape base;
    double width;
    double height;
};

static double rect_area(const struct Shape *self) {
    const struct Rect *r = (const struct Rect *)self;
    return r->width * r->height;
}

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

static const struct ShapeVtable rect_vtable = {
    .area = rect_area,
    .describe = rect_describe,
    .destroy = circle_destroy,  /* same free logic */
};

struct Shape *rect_create(double w, double h) {
    struct Rect *r = malloc(sizeof(*r));
    if (!r) return NULL;
    r->base.vtable = &rect_vtable;
    r->width = w;
    r->height = h;
    return &r->base;
}

int main(void) {
    struct Shape *shapes[] = {
        circle_create(5.0),
        rect_create(4.0, 6.0),
        circle_create(1.5),
    };

    for (int i = 0; i < 3; i++) {
        shapes[i]->vtable->describe(shapes[i]);
        printf("Area: %.2f\n\n", shapes[i]->vtable->area(shapes[i]));
    }

    for (int i = 0; i < 3; i++) {
        shapes[i]->vtable->destroy(shapes[i]);
    }

    return 0;
}
Circle with radius 5.00
Area: 78.54

Rectangle 4.00 x 6.00
Area: 24.00

Circle with radius 1.50
Area: 7.07

The base struct contains a pointer to a vtable. Each concrete type provides its own vtable. Calling shape->vtable->area(shape) dispatches to the correct implementation.

Common Pitfalls

  • Forgetting the parentheses. int *fn(int) is a function returning int*. int (*fn)(int) is a pointer to a function returning int. The parentheses are critical.
  • Calling through a NULL function pointer. Always check that the pointer is not NULL before calling, especially for optional callbacks.
  • Mismatched signatures. If the function pointer type expects int (*)(int, int) and you assign a function with signature int func(int), the behavior is undefined.
  • Casting function pointers. Casting between incompatible function pointer types is undefined behavior. Always match the expected signature exactly.
  • Signal handler safety. Signal handlers run asynchronously. Calling printf, malloc, or most standard library functions from a signal handler is undefined behavior. Only modify volatile sig_atomic_t variables and call async-signal-safe functions.

Key Takeaways

  • A function pointer stores the address of a function and allows calling it indirectly.
  • Use typedef to make function pointer types readable.
  • Callbacks decouple the caller from the implementation. The void *user_data pattern provides context without globals.
  • Dispatch tables (arrays of function pointers) replace switch statements and scale easily.
  • qsort is the textbook example of callbacks in the C standard library.
  • Structs of function pointers implement polymorphism and virtual dispatch, the same mechanism C++ uses under the hood.