C Function Pointers & Callbacks: Build Event-Driven Systems and Plugin Architectures

C Function Pointers & Callbacks: Build Event-Driven Systems and Plugin Architectures
Table of Contents
- Functions Are Memory Addresses
- Declaring Function Pointer Variables
- Assigning and Calling Through Pointers
- typedef: Cleaning Up the Syntax
- Functions as Parameters: Callbacks
- Using qsort with a Comparator Callback
- Dispatch Tables: O(1) Event Handling
- The vtable Pattern: Manual Polymorphism
- Plugin Architecture in C
- Performance Characteristics
- Frequently Asked Questions
- Key Takeaway
Functions Are Memory Addresses
In C, when you define a function like int add(int a, int b), the compiled code for that function is placed in a specific location in your program's text (code) segment. The function's name is a convenient label for that location's address.
Just as int x = 5 has an address (&x), a function int add(int a, int b) has an address (add or equivalently &add). A function pointer is a variable that stores this address:
Declaring Function Pointer Variables
The syntax for a function pointer follows a specific pattern: ReturnType (*name)(ParameterTypes):
The (*name) with parentheses is essential. Without them, int *name(int, int) declares a function that returns int* — not a pointer to a function.
Assigning and Calling Through Pointers
Both op(5, 3) and (*op)(5, 3) are valid — the C standard permits both. The modern style omits the * for cleaner code.
typedef: Cleaning Up the Syntax
Function pointer syntax is notoriously verbose. Professional code almost always uses typedef to create a clean type alias:
With typedef, MathOperation my_op = add reads almost like a regular variable declaration — a massive improvement in readability.
Functions as Parameters: Callbacks
The canonical use of function pointers is as callback parameters — passing behavior to a function rather than hardcoding it:
This is the Strategy Pattern in C — the algorithm (the transformer) is swapped out at runtime without changing the container logic (apply_to_array). This exact pattern appears in the C standard library's qsort.
Using qsort with a Comparator Callback
qsort from <stdlib.h> sorts any array using a user-provided comparator:
Dispatch Tables: O(1) Event Handling
Instead of a long switch statement mapping event codes to handlers, a dispatch table (an array of function pointers) achieves O(1) dispatch:
This pattern is used in operating system interrupt descriptor tables (IDT), protocol handlers in network stacks, and GUI event systems.
The vtable Pattern: Manual Polymorphism
C++ virtual methods are implemented as function pointer tables (vtables). In C, you can replicate this explicitly. This is exactly how the Linux Virtual File System (VFS) works — struct file_operations contains function pointers for read, write, ioctl, etc.:
This is the foundation of how GObject (used in GTK, GNOME), GLib, and the Linux kernel implement OOP in C.
Plugin Architecture in C
Function pointers via dlopen/dlsym (POSIX) enable dynamic plugin loading:
This is how Apache modules, Nginx modules, and countless plugin-based systems load behavior at runtime without recompiling the host application.
Performance Characteristics
A function pointer call ((*fp)(args)) is an indirect call:
- Compiles to
CALL [register]on x86-64. - The CPU's branch predictor cannot predict the destination as easily as a direct call.
- Typical overhead: 1-5 nanoseconds per call (negligible for most use cases).
- In tight inner loops called millions of times per second, direct function calls or inlining are preferred.
Modern CPUs have improved indirect branch prediction (especially post-Spectre mitigations), so the overhead is rarely significant outside of the most performance-critical microarchitecture-sensitive code.
Frequently Asked Questions
Can a function pointer to a function with the wrong signature cause crashes?
Yes — this is undefined behavior. If you cast a function of type void f(int) to void (*)(double) and call it, the argument passing convention will be wrong and the result is unpredictable. Always ensure the function pointer type precisely matches the function's actual signature.
Can I store NULL in a function pointer?
Yes — NULL (or in C23, nullptr) is a valid null function pointer. You should check for NULL before calling: if (callback != NULL) callback(args);. This is the standard pattern in callback-based APIs where a callback is optional.
Are there any tools for finding function pointer bugs?
Clang's -fsanitize=function flag enables Function Sanitizer, which detects calls through function pointers with mismatched types. AddressSanitizer also detects NULL function pointer dereferences.
What is the signal() function signature — a famous example?
signal() from <signal.h> has one of the most complex function pointer signatures in the standard library: void (*signal(int sig, void (*func)(int)))(int). It takes a signal number and a handler pointer, and returns a pointer to the previous handler. Most code uses typedef void (*SigHandler)(int) to make this readable.
Key Takeaway
Function pointers are the secret weapon of Extensible C Systems. They transform logic from a compile-time fixed thing into a runtime-configurable variable. By mastering callbacks, dispatch tables, and vtable patterns, you gain the ability to build systems that are as architecturally sophisticated as any C++ or Java framework — but with zero language overhead.
Read next: Void Pointers & Generic C: Type-Erased Programming →
Part of the C Mastery Course — 30 modules from C basics to expert systems engineering.
