[C Series 17] Understanding Function Pointers and Callback Patterns

한국어 버전

After wrapping your head around storage duration and scope in part 16, it's time to treat functions themselves like values so you can compose more flexible code. A function pointer stores the memory address where a function begins, and a callback is the pattern of passing that address to other code so it can invoke the function later.

New Terms in This Lesson

  1. Function Pointer: A pointer that stores the entry address of a function
  2. Callback: Passing a function as an argument and letting someone else call it later
  3. Signature: A function's return type and parameter list without the name
  4. Strategy Pattern: Injecting behavior chosen outside the callee

Key Ideas

Study Notes

  • Estimated time: 60–75 minutes
  • Prereqs: Pointer basics, declaring/defining functions, iterating arrays
  • Goal: Declare/assign/invoke function pointers and design callback-based flows

When you see a function pointer, check the signature first. int (*handler)(const char *msg) can look intimidating, but you can unpack it in steps: the pointer name is handler, the pointed-to function takes const char *msg, and the return type is int. In the beginning, pretend it's "a variable that stores the location of a function instead of a number" and everything becomes simpler.

We'll tackle the following steps:

  • Declare function pointers and understand why you can assign both &func and func
  • Group multiple function pointers in an array and iterate through them
  • Accept callbacks as parameters to swap sorting strategies or event handlers
  • Pass extra data with void *context so callbacks can interpret their environment

Code Walkthrough

Function Pointer Basics

#include <stdio.h>

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

int cube(int n) {
    return n * n * n;
}

int main(void) {
    int (*operation)(int);

    operation = square;
    printf("square(3) = %d\n", operation(3));

    operation = cube;
    printf("cube(3) = %d\n", operation(3));

    return 0;
}

You can write operation = square; without the ampersand because function names decay to their starting addresses. Calling operation(3) jumps into whichever function the pointer currently references.

Arrays of Function Pointers

#include <stdio.h>

typedef int (*calc_fn)(int);

int add_one(int n) {
    return n + 1;
}

int double_num(int n) {
    return n * 2;
}

int negate(int n) {
    return -n;
}

int main(void) {
    calc_fn steps[3] = {add_one, double_num, negate};
    int value = 5;
    int i;

    for (i = 0; i < 3; i++) {
        value = steps[i](value);
        printf("[%d] -> %d\n", i, value);
    }

    return 0;
}

typedef keeps declarations tidy. Here, an array named steps stores three operations. As long as they share the same signature, you can loop through them like any other array.

Switching Sort Strategies with Callbacks

#include <stdio.h>

typedef int (*compare_fn)(int a, int b);

int ascending(int a, int b) {
    return (a > b) - (a < b);
}

int descending(int a, int b) {
    return (b > a) - (b < a);
}

void selection_sort(int *arr, int len, compare_fn cmp) {
    int i, j, min_idx;

    for (i = 0; i < len - 1; i++) {
        min_idx = i;
        for (j = i + 1; j < len; j++) {
            if (cmp(arr[j], arr[min_idx]) < 0) {
                min_idx = j;
            }
        }

        if (min_idx != i) {
            int temp = arr[i];
            arr[i] = arr[min_idx];
            arr[min_idx] = temp;
        }
    }
}

int main(void) {
    int data[5] = {42, 7, 98, 13, 55};

    selection_sort(data, 5, ascending);
    printf("ascending: %d %d %d %d %d\n",
           data[0], data[1], data[2], data[3], data[4]);

    selection_sort(data, 5, descending);
    printf("descending: %d %d %d %d %d\n",
           data[0], data[1], data[2], data[3], data[4]);

    return 0;
}

Inside the sort function, the cmp callback decides ordering. Swap out the comparison and you get a different behavior while reusing the same sorting logic. The usual agreement is simple: return a negative value if the first argument should come first, 0 if the two values are equal, and a positive value otherwise.

Passing Context into Callbacks

#include <stdio.h>

typedef void (*log_fn)(const char *msg, void *context);

typedef struct {
    int warning_threshold;
} logger_config;

void console_logger(const char *msg, void *context) {
    logger_config *cfg = (logger_config *)context;
    printf("[warn>= %d] %s\n", cfg->warning_threshold, msg);
}

void monitor_temperature(int readings[], int len, log_fn logger, void *ctx) {
    int i;
    logger_config *cfg = (logger_config *)ctx;

    for (i = 0; i < len; i++) {
        if (readings[i] >= cfg->warning_threshold) {
            char buffer[64];
            snprintf(buffer, sizeof(buffer), "sensor %d: %dC", i, readings[i]);
            logger(buffer, ctx);
        }
    }
}

int main(void) {
    int temps[6] = {32, 48, 51, 37, 60, 42};
    logger_config cfg = {45};

    monitor_temperature(temps, 6, console_logger, &cfg);
    return 0;
}

If a callback receives only the address of a function, it lacks context. What if the callback needs to remember extra settings such as a warning threshold or a label? That is where void *context comes in: it is a small data bag the caller can pass along. The callback then casts it back to the proper type. Because void * carries no type information, both sides must agree on the structure, and casting it to the wrong type is a bug the compiler may not catch. This pattern is everywhere in GUI event loops and network libraries.

Why It Matters

  • Function pointers build intuition that code can move around like data.
  • Callbacks separate library code from user-provided behavior.
  • You can implement the strategy pattern at C's level by injecting different operations.
  • OS APIs, signal handlers, and timer libraries rely heavily on these patterns.

Practice in CodeSandbox

The sandbox below uses CodeSandbox's Universal starter. For C, the key learning loop is still compile and run in the terminal, so recreate the lesson code as a source file and repeat that cycle directly.

Live Practice

C Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Universal starterCterminal
  1. Fork the starter and create a practice file such as hello.c
  2. Paste in the lesson code and compile it if clang or gcc is available
  3. Edit the code, rebuild it, and compare the new output

For C, the terminal build loop matters more than a browser preview. Compiler availability can vary by environment, so first confirm that clang or gcc is present in the Universal starter.

Practice

  • Follow along: Run the function-pointer-array example and log how changing the order affects the output.
  • Extend: Recreate a qsort-style interface that takes the array length and comparator as arguments while hiding the algorithm.
  • Debug: Intentionally pass a callback with the wrong signature and record the compiler warnings.
  • Completion check: Write your own function-pointer declaration and a function that accepts a callback, then invoke it.

Wrap-Up

Once you understand function pointers and callbacks, C code can change behavior dynamically. Next up: bitwise operations that cultivate hardware-level instincts.

💬 댓글

이 글에 대한 의견을 남겨주세요