CFoundations

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

TT
TopicTrick Team
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

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:

c
mermaid

Declaring Function Pointer Variables

The syntax for a function pointer follows a specific pattern: ReturnType (*name)(ParameterTypes):

c

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

c

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:

c

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:

c

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:

c

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:

c

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.:

c

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:

c

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.