Function Pointers and the Callback Pattern
If pointers are the most powerful feature of C, then function pointers are arguably the most blood-pressure-raising aspect of the pointer world. But honestly, once you master them, you will find they are one of the few mechanisms in C that allow you to write code that is "flexible enough to not feel like C"—callbacks, event-driven programming, the strategy pattern; these concepts that sound like they belong in high-level languages are all supported in C thanks to function pointers.
We have systematically covered various pointer usages in previous tutorials. In this chapter, we will tackle this hard nut: function pointers. We will start with declarations and basic usage, move on to arrays of function pointers and the callback pattern, and finally look at the comfortable improvements C++ has made in this area.
Learning Objectives
- After completing this chapter, you will be able to:
- [ ] Understand function pointer declaration syntax and use it correctly.
- [ ] Use
typedefto simplify complex function pointer types.- [ ] Implement a callback-based sorting interface similar to
qsort.- [ ] Build a simple event dispatch system.
- [ ] Understand the corresponding relationships in C++ regarding
std::function, lambdas, and function objects.
Environment Setup
All code in this chapter has been verified in the following environment:
- Operating System: Linux (Ubuntu 22.04+) / WSL2 / macOS
- Compiler: GCC 11+ (Confirm version via
gcc --version) - Compiler Flags:
gcc -Wall -Wextra -std=c11(Enable warnings, specify C11 standard) - Verification: All code can be compiled and run directly.
Step 1 — Using Functions as Data
In C, a function compiles into a segment of machine instructions residing in the code section of memory. Since it resides in memory, it has an address—the function name itself (when not followed by invocation parentheses) is a pointer to this address. We can store this address and use it to invoke the function when needed.
Learn to Declare Function Pointers First
The declaration syntax for function pointers is notoriously one of C's "anti-human" designs. Let's grit our teeth and look at it:
// Declaration: ptr is a pointer to a function taking two ints and returning an int
int (*ptr)(int, int);Let's break down this declaration: ptr is a pointer (because *ptr is enclosed in parentheses). It points to a function that accepts two int parameters and returns an int. Those parentheses cannot be omitted—if you write int *ptr(int, int), the compiler interprets it as "a function named ptr that returns an int pointer," which is completely different.
⚠️ Warning: When declaring a function pointer, the parentheses around
(*ptr)must never be omitted. Omitting them turns it into a declaration of a function returning a pointer. The compiler might not error, but the behavior will be completely different. This is one of the most common mistakes for newcomers.
Once we have the pointer, assignment and invocation are natural:
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
// ptr points to the function 'add'
int (*ptr)(int, int) = add;
// Call the function via the pointer
int result = ptr(10, 20);
printf("Result: %d\n", result);
return 0;
}Output:
Result: 30In most contexts, a function name implicitly converts to a function pointer, just like an array name "decays" into a pointer to its first element. Therefore, add does not need the address-of operator &. When calling, ptr(10, 20) and (*ptr)(10, 20) are completely equivalent—the C standard states that function pointers are automatically dereferenced.
Use typedef to Make Declarations Readable
The syntax for declaring function pointers is unfriendly. Once types get complex or need to be used in multiple places, a screen full of int (*)(int, int) is torture. typedef is our savior—it doesn't create a new type but gives an alias to an existing one:
// Define an alias named 'Operation' for 'int (*)(int, int)'
typedef int (*Operation)(int, int);
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int main() {
// Now the declaration is much cleaner
Operation op = add;
printf("10 + 20 = %d\n", op(10, 20));
op = sub;
printf("10 - 20 = %d\n", op(10, 20));
return 0;
}It is highly recommended to use typedef to manage function pointers whenever they appear in a project. Especially in API design for callback interfaces, typedef not only simplifies writing function signatures but also improves the self-documenting nature of header files.
Step 2 — Batch Dispatching with Arrays of Function Pointers
Function pointers can do more than just store a single function address—by stuffing multiple function pointers into an array, we can use an index to select which function to invoke. This pattern is very useful in scenarios like command dispatching or state machine jump tables:
Expand (21 lines)Collapse
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }
// Array of function pointers
int (*operations[])(int, int) = { add, sub, mul, div };
int main() {
int a = 10, b = 5;
// Iterate through the operation table
for (int i = 0; i < 4; i++) {
int result = operations[i](a, b);
printf("Operation %d result: %d\n", i, result);
}
return 0;
}Output:
Operation 0 result: 15
Operation 1 result: 5
Operation 2 result: 50
Operation 3 result: 2This "operation table" pattern is common in embedded firmware. For example, if you have a set of serial commands, each corresponding to a handler function, you can index these function pointers by command ID. When a command is received, dispatching is done in a single line: handlers[cmd_id](data).
⚠️ Warning: When using an array of function pointers for dispatching, always check if the index is out of bounds. If
cmd_idexceeds the array range, you will access either a garbage address orNULL—calling it directly will cause a segmentation fault.
Step 3 — Master the Callback Pattern
Where function pointers truly shine is in callbacks. The core idea of a callback is simple: I pass you a function's address, and you call it on my behalf at the appropriate time. In plain English, it means "call me back"—the caller does not execute a piece of logic directly, but instead "registers" this logic with the callee, who triggers it when needed.
Understanding Callbacks via qsort
The C standard library's qsort function is a textbook example of the callback pattern:
Expand (23 lines)Collapse
#include <stdio.h>
#include <stdlib.h>
// Comparison function: returns <0, 0, or >0
int compare_ints(const void *a, const void *b) {
int arg1 = *(const int *)a;
int arg2 = *(const int *)b;
return (arg1 > arg2) - (arg1 < arg2);
}
int main() {
int data[] = { 5, 2, 9, 1, 5, 6 };
int n = sizeof(data) / sizeof(data[0]);
// Pass the function pointer to qsort
qsort(data, n, sizeof(int), compare_ints);
for (int i = 0; i < n; i++)
printf("%d ", data[i]);
printf("\n");
return 0;
}Output:
1 2 5 5 6 9The first three parameters are the array start address, the number of elements, and the size of each element. The last parameter is a pointer to a comparison function—whenever qsort needs to compare two elements during the sorting process, it calls this function.
int compare_desc(const void *a, const void *b) {
return compare_ints(b, a); // Reverse order
}
// ... inside main ...
qsort(data, n, sizeof(int), compare_desc);Output:
9 6 5 5 2 1The sorting logic itself (the implementation of qsort) remains completely unchanged. We simply swapped the comparison function, and the sorting result is completely different. This is the power of callbacks—decoupling algorithms from strategies.
⚠️ Warning:
qsort's comparison function receivesconst void*. The return value follows the convention: "left less than right returns negative, equal returns 0, left greater than right returns positive." If you write the comparison logic backwards, the result will be unsorted—and there will be no compile-time warnings.
Step 4 — Build an Event Dispatch System
Let's combine function pointers, typedef, and arrays of function pointers to build a simple event dispatch system:
Expand (60 lines)Collapse
#include <stdio.h>
#include <stdbool.h>
// Define callback type: event ID and user data
typedef void (*EventHandler)(int event_id, void *user_data);
// Event handler table
#define MAX_EVENTS 10
typedef struct {
int id;
EventHandler callback;
void *user_data;
} EventEntry;
EventEntry event_table[MAX_EVENTS];
int event_count = 0;
// Register an event
void subscribe(int id, EventHandler handler, void *user_data) {
if (event_count < MAX_EVENTS) {
event_table[event_count].id = id;
event_table[event_count].callback = handler;
event_table[event_count].user_data = user_data;
event_count++;
}
}
// Trigger an event
void publish(int id) {
for (int i = 0; i < event_count; i++) {
if (event_table[i].id == id && event_table[i].callback != NULL) {
event_table[i].callback(id, event_table[i].user_data);
}
}
}
// --- User Code ---
void on_led_on(int event_id, void *user_data) {
printf("LED ON event triggered! User data: %d\n", *(int*)user_data);
}
void on_led_off(int event_id, void *user_data) {
printf("LED OFF event triggered!\n");
}
int main() {
int context = 42;
subscribe(1, on_led_on, &context);
subscribe(2, on_led_off, NULL);
printf("Publishing event 1...\n");
publish(1);
printf("Publishing event 2...\n");
publish(2);
return 0;
}This is a minimal viable event system. void* user_data acts as the "universal glue" here—whatever extra state information the callback needs, the caller passes it in via this void* pointer. This design is ubiquitous in embedded SDKs. For example, the callback registration interfaces in the STM32 HAL library are essentially this pattern.
C++ Connection
C++ has made multi-level improvements in this direction, from basic function objects to modern lambdas and std::function.
Function Objects (Functors): Overload operator() for a class so its instances can be called like functions. Compared to C's function pointers, the biggest advantage of function objects is that they can carry state.
Lambda Expressions (C++11): Anonymous function objects defined inline at the call site, supporting capture of external variables (closures). This is impossible to achieve in the world of C function pointers.
std::function (C++11): A generic, type-safe function wrapper that can hold any callable target: function pointers, function objects, lambdas, etc. It unifies the interface of all callable objects.
Template Strategy Pattern: Strategies are determined at compile time, resulting in zero runtime overhead, but increasing compilation time.
From C's function pointers to C++'s lambdas and std::function, the core idea is consistent—parameterizing "behavior". C achieved the most basic version with function pointers, while C++ added type safety, closures, and a unified callable object interface on top of that.
Summary
Function pointers are the core mechanism for implementing callbacks and the strategy pattern in C. The declaration syntax is indeed unfriendly, but once managed with typedef, they are very practical. Arrays of function pointers enable table-driven dispatch logic. The callback pattern is clearly illustrated through the classic case of qsort—the algorithm framework and specific strategy are decoupled via function pointers. The event dispatch system is a direct application of callbacks in event-driven programming.
Key Takeaways
- [ ] Function names implicitly convert to function pointers in most contexts.
- [ ] Parentheses in declaration syntax cannot be omitted:
int (*ptr)(int)vsint *ptr(int). - [ ]
typedefis the best practice for managing complex function pointer types. - [ ] Arrays of function pointers can implement table-driven command/state dispatch.
- [ ] The core of callbacks is "algorithm invariant, strategy replaceable."
- [ ]
void*provides generic capabilities at the cost of type safety; C++ templates andstd::functionsolve this issue.
Exercises
Exercise 1: Generic Sorting Interface
Following the interface design of qsort, implement your own generic insertion sort function. Use it to sort an int array (ascending and descending) and a string array (lexicographical order):
// TODO: Implement this function
void my_isort(void *base, size_t n, size_t size,
int (*compar)(const void *, const void *));Exercise 2: Event Dispatch System Extension
Based on the event dispatch system in this chapter, support registering multiple callbacks for the same event (a callback chain) and support unregistering callbacks. Think about this: what happens if a handler in the chain modifies the linked list structure during execution?
Exercise 3: Simple Command-Line Calculator
Use an array of function pointers to implement a command-line calculator supporting addition, subtraction, multiplication, division, and modulo operations. Select the corresponding function based on the user-inputted operator.
// Hint: Define a function pointer array and index it by operator type
// double (*operations[])(double, double) = { ... };