Function Pointers in C — NULL Crash in Embedded Firmware
Missing config flag leaves function pointer NULL, triggering hard fault at 0x00000000.
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
- Function pointers store the address of a function, not the function itself
- Syntax:
int (ptr)(int, int)— parentheses aroundptrare mandatory - Use
typedefto make function pointer types readable - Callbacks allow passing a function as an argument — used in
qsort, event handlers - Dispatch tables replace long switch/if-else chains with an array of function pointers
- Biggest pitfall: calling a NULL function pointer causes a segfault — always check before calling
Imagine you hire a contractor and instead of telling them exactly how to do every step, you hand them a card with a phone number to call when they finish a task — whoever answers decides what happens next. A function pointer is that phone number. It's not the function itself; it's the address where the function lives in memory, so you can hand it to someone else and say 'call this when you're ready.' This lets your code be flexible — the same contractor can call your plumber, your electrician, or your interior designer depending on what card you give them.
Every non-trivial C program eventually hits a wall: you need a piece of code to behave differently depending on context, but you don't want to litter your logic with a dozen if-else branches. Sort algorithms need comparison logic. Event loops need to dispatch to different handlers. Plugin systems need to call code that didn't exist when the core was compiled. In every one of these cases, function pointers are the tool C gives you — and understanding them is what separates C programmers who write clever hacks from those who write clean, extensible systems.
The problem function pointers solve is simple: in C, functions are not first-class values you can pass around the way you'd pass an integer. But their addresses are. A function pointer stores that address, which means you can store a function in a variable, pass it as an argument, return it from another function, or keep a whole table of them. This is how the C standard library's qsort works, how operating system kernels register interrupt handlers, and how game engines implement entity behavior without a class hierarchy.
By the end of this article you'll be able to declare and call function pointers without second-guessing the syntax, build a working callback system, construct a dispatch table that replaces a cascade of if-else statements, and spot the two errors that burn every developer the first time they use function pointers in production code.
What a Function Pointer Actually Is
A function pointer stores the address of a function in memory, allowing you to call that function indirectly through the pointer. In C, a function name decays to a pointer to the function's entry point, just as an array name decays to a pointer to its first element. The declaration syntax mirrors the function signature: return_type (*ptr_name)(parameter_types). This indirection is the core mechanic — you can pass functions as arguments, store them in arrays, or assign them at runtime. Function pointers have a fixed size (typically 4 or 8 bytes, matching the platform's pointer width) and can be compared for equality. Dereferencing a NULL function pointer causes an immediate crash — often a hard fault on embedded systems — because the processor jumps to address zero. This is not a recoverable error; it's a silent, catastrophic failure. Use function pointers when you need runtime polymorphism without C++ overhead: callback registration, state machines, command dispatch tables, or plugin architectures. In embedded firmware, they are essential for decoupling hardware drivers from application logic — for example, a timer driver that calls a user-defined callback on overflow. The cost is one extra indirection per call (negligible on most MCUs) and the risk of NULL dereference if not validated.
Declaring and Calling a Function Pointer — Getting the Syntax Right Once and For All
The syntax for function pointers trips people up because the asterisk belongs to the name, not the return type. Read the declaration from the inside out: the name of the variable is in the middle, wrapped in parentheses with an asterisk, and the surrounding parts describe what function signature it can point to.
For a function that takes two ints and returns an int, the pointer type is: int (operation)(int, int). That parentheses around operation is mandatory — without it, int operation(int, int) is a completely different thing: a function named operation that returns int .
Once you have the pointer, calling it is straightforward. Modern C allows you to call it directly as operation(a, b) — the compiler knows it's a pointer and handles the dereference. The older explicit-dereference syntax (*operation)(a, b) also works and makes the pointer nature more obvious. Both styles are valid; pick one and be consistent within a codebase.
typedef int (*MathOperation)(int, int) once means every subsequent use is just MathOperation. When this type appears in a struct, a parameter list, and a return type — and it will — you'll be very glad you did. It also makes the code self-documenting: MathOperation communicates intent far better than the raw pointer syntax.ptr(args) or (*ptr)(args) — be consistent.Callbacks — Passing Functions as Arguments the Way qsort Does
A callback is nothing more than a function pointer you pass to another function so that function can call yours at the right moment. It's the foundational pattern behind event-driven systems, custom sorting, plugin architectures, and async I/O notification.
The C standard library's qsort is the example every C programmer meets first. You hand qsort your array and a comparator — a function pointer that tells qsort how to decide which of two elements is 'less than' the other. qsort doesn't care what you're sorting or how you define order; it just calls your comparator whenever it needs to compare two elements.
Building your own callback-based API follows the same pattern. You design a function that accepts a function pointer parameter. Callers provide different functions to customize behavior. Your core logic stays untouched.
Dispatch Tables — Replacing if-else Chains With an Array of Function Pointers
Once you can store a function pointer in a variable, you can store them in an array. An array of function pointers is called a dispatch table (or jump table), and it's one of the most useful patterns in systems programming.
Consider a simple calculator that handles four operations. The naive approach is a chain of if-else or a switch statement. That works for four operations, but what about forty? You'd have forty branches, and adding a new operation means touching the dispatcher every single time.
A dispatch table maps an index (or enum value) directly to a function. Adding a new operation is adding one entry to the table. The dispatcher doesn't change at all.
Function Pointers in Structs — How C Fakes Object-Oriented Design
If you put function pointers inside a struct, the struct gains behavior — it's not just data anymore. This is exactly how C++ implements virtual functions under the hood (the vtable).
This pattern works by defining a struct that holds function pointer fields. Different 'instances' can point their pointers at different implementations. Code that operates on the struct calls the function through the pointer without knowing which implementation it's talking to.
This matters because C is used where C++ isn't an option — microcontrollers and kernels. Knowing how to build a clean interface with function pointers lets you write reusable C code rather than copying logic for every new variant.
State Machines with Function Pointers — Clean Event Handling Without Switch Statements
State machines are everywhere — protocol handlers, UI navigation, game states. The textbook approach uses a switch statement with a state variable. That works, but as states grow, the switch becomes a maintenance nightmare. Function pointers offer a cleaner alternative: each state is a function, and the state machine's current state pointer is a function pointer. Transitioning to a new state is as simple as reassigning the pointer.
This pattern is common in embedded systems where memory is tight and you need deterministic timing. Each state function receives events and returns the next state function pointer. The main loop simply calls the current state function in a tight loop, consuming no additional stack depth.
Implementing this avoids the risk of missing a state in a switch, keeps state logic isolated, and makes adding new states trivial — just write a new function and register it.
Where Functions Live — The Address You Never Knew You Needed
Every function you write sits in memory at a specific address. Just like a variable, a function name without parentheses evaluates to its entry point. This isn't trivia — it's the whole reason function pointers exist. When you write multiply instead of , you're grabbing an address, not calling anything.multiply()
Competitor tutorials love to show you the syntax, but they skip the why. The address lets you store, pass, and invoke logic at runtime. That's what turns static C code into something dynamic. You don't need classes for polymorphism; you just need a pointer-sized value that points to executable instructions.
If you're debugging a crash and the callstack points to garbage, it's often because someone corrupted a function pointer. Know the address. Trust nothing.
void* and back. The C standard doesn't guarantee it works. On some architectures like CHERI or systems with pointer authentication, this will blow up at runtime.Function Pointers in C++ — The Same Beast, Sharper Teeth
C++ inherits C's function pointers wholesale, but the language adds a layer of complexity that'll bite you if you're careless. Member function pointers? Different syntax, different cost. Lambdas? They can decay to function pointers, but only if they capture nothing — otherwise you're looking at std::function and heap allocations.
The core semantics haven't changed: a function pointer is still just an address. But C++ templates, overloads, and namespaces mean you can't always grab an address with just the bare name. You might need & explicitly to disambiguate an overloaded function. If you see "reference to overloaded function could not be resolved", that's your cue.
Bottom line: every trick you learned in C works here. The hazards come from C++ features that pretend to be simpler than they are. Test your assumptions with std::is_same or a static_assert — let the compiler tell you the truth.
auto to store member pointers: auto action = &Server::start;. The compiler deduces the correct type every time, and you avoid the esoteric (svr.*ptr)() syntax confusion.& operator and a special call syntax — they are not interchangeable with free function pointers.Function Pointers for Plugin Systems — Loading Unknown Code at Runtime
Static linking is for people who rebuild their entire product to add one feature. Real systems load functionality at runtime via dynamic libraries. Function pointers are the glue that makes this possible.
When you call on a shared object, the OS hands you a handle. You then use dlopen() to pluck a function by name out of that binary blob. The symbol resolves to a memory address — exactly the same kind of address you'd get from dlsym()&myFunction. Cast that to the correct function pointer type, and you're calling code that didn't exist when your program was compiled.
This is how game engines load mods, how editors support plug-ins, and how production servers swap logging backends without restart. The contract is simple: the plugin exposes a function with a known signature, usually int init(plugin_api* api). Your host calls it through a pointer. No headers, no recompilation. Just a void pointer cast and a handshake.
Memory management is your problem. The plugin's code stays in RAM until you call dlclose(). Keep the handle around if you plan to unload later — dangling function pointers are a crash just waiting for a holiday weekend.
void* on POSIX. The C standard says it's undefined behavior. Use a union or memcpy the bytes. Real-world code does it anyway because every compiler allows it, but don't act surprised when a static analyzer flags it.dlopen + dlsym is your backdoor for plugin loading.Polymorphism Without the vtable — Manual Dispatch in Embedded Systems
C++ vtables are a luxury. They cost memory per class, per instance. When your firmware has 2KB of RAM, you can't afford that. So you build polymorphism by hand with function pointers embedded in structs — one pointer per method, one table per object.
You define a struct with data and one function pointer per operation. A 'constructor' function fills those slots, then returns the struct. Each 'method' takes a pointer to that struct as its first argument — hello, this. The caller doesn't know whether it's talking to a UART driver or a SPI driver. It just calls dev->write(dev, buffer, len) through the pointer.
No casting, no inheritance, no virtual dispatch overhead. The function pointer is a direct jump. Compare that to a switch on device type, which the compiler may or may not optimize into a jump table. Manual dispatch always wins on worst-case latency. This is why every RTOS scheduler uses function pointers for task entry points.
The tradeoff: you type out the dispatch table yourself. No compiler-generated wrappers. But you also see exactly how much flash each virtual call costs. In constrained environments, that transparency is worth more than syntactic sugar.
NULL Function Pointer in Embedded Firmware
if (handler) handler(); else default_handler();.- Always initialize function pointers to a safe default, not just NULL.
- Add a NULL guard before every function pointer call — it costs one branch and prevents hard-to-diagnose crashes.
- Treat uninitialized function pointers as undefined behavior — they will crash eventually.
print ptr — if 0x0, look for missing initialization. Use a conditional breakpoint to catch the assignment.typedef and compile with -Werror to catch mismatches.volatile on the pointer or disable optimization for that file. Alternatively, check that the pointer is not optimized away.print my_func_ptrinfo functions my_func_ptr (to see if the target function exists in symbol table)if (my_func_ptr) my_func_ptr();Key takeaways
int (op)(int, int) is a pointer to a function; int op(int, int) is a function that returns a pointer.Common mistakes to avoid
3 patternsMissing parentheses around the pointer name in declaration
int handler(int) instead of int (handler)(int). The compiler treats it as a function returning an int pointer, not a pointer to a function. The code compiles but calling handler(5) or dereferencing incorrectly causes undefined behavior.(*name) to ensure the asterisk modifies the variable name, not the return type. Use a typedef to avoid repeating the complex syntax.Calling a NULL function pointer
if (ptr) ptr(args);. Consider providing a default no-op function for empty states.Signature mismatch when casting function pointers
void ()(int) function into a void ()(float) pointer via a cast. The code may compile with a warning, but calling it corrupts the stack because the arguments are interpreted differently.Interview Questions on This Topic
What is the difference between `void *f(int)` and `void (*f)(int)`?
f is a function taking an int and returning a void pointer. The second is a pointer to a function: f is a variable that can hold the address of a function taking an int and returning void. In the second case, you need to assign an actual function before calling f(5).Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
That's C Basics. Mark it forged?
8 min read · try the examples if you haven't