Mid-level 8 min · March 06, 2026

Function Pointers in C — NULL Crash in Embedded Firmware

Missing config flag leaves function pointer NULL, triggering hard fault at 0x00000000.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Function pointers store the address of a function, not the function itself
  • Syntax: int (ptr)(int, int) — parentheses around ptr are mandatory
  • Use typedef to 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
✦ Definition~90s read
What is Function Pointers in C?

A function pointer is a variable that stores the memory address of a function, allowing you to call that function indirectly through the pointer. In C, functions themselves are not first-class objects—you can't pass them around or store them directly—but function pointers give you that capability.

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.

They exist because embedded systems and low-level code often need dynamic dispatch: deciding at runtime which function to execute, without the overhead of a virtual machine or runtime type system. The cost is a single indirection through the pointer, which on ARM Cortex-M or AVR microcontrollers is typically 2–4 cycles and zero additional RAM beyond the pointer itself.

Function pointers solve real problems in constrained environments. They replace long if-else or switch chains with dispatch tables—arrays of function pointers indexed by an enum or integer—which are faster, more maintainable, and easier to extend. They enable callback mechanisms like qsort's comparator argument, where a generic sorting function calls back into your code without knowing your data types.

They also let C approximate object-oriented patterns: putting function pointers inside structs creates virtual method tables (vtables) that mimic polymorphism, used extensively in real-time operating systems like FreeRTOS (task control blocks) and in hardware abstraction layers (HALs) from STM32 and NXP.

But function pointers come with sharp edges. The most dangerous is the NULL crash: calling a function pointer that hasn't been initialized or has been set to NULL. On bare-metal embedded systems, there's no OS to catch the segfault—the processor jumps to address 0x00000000, which is typically the reset vector or unmapped memory, causing a hard fault or watchdog reset.

This is a common bug in state machine implementations and callback registrations where a pointer is declared but never assigned. The fix is defensive: always initialize function pointers to a safe default (like a no-op function), validate them before calling, and use static analysis tools (e.g., PC-lint, Coverity) to catch uninitialized paths.

When you need dynamic dispatch but can't afford the risk, consider alternatives like jump tables (computed goto in GCC) or explicit state enums with switch statements—though those lose the flexibility of runtime-replaceable handlers.

Plain-English First

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.

NULL Function Pointer = Hard Fault
Calling a NULL function pointer does not return an error — it jumps to address 0x00000000, triggering an immediate hard fault on ARM Cortex-M and most embedded targets.
Production Insight
A production UART driver stored a callback pointer in a struct; a race condition in initialization left it NULL. The first interrupt fired, jumped to zero, and locked the system with no log output.
Symptom: system hangs silently on first UART RX interrupt — no crash dump, no watchdog reset because the fault handler was not configured.
Rule: Always initialize function pointers to a safe default (e.g., a no-op function) and validate non-NULL before calling in interrupt context.
Key Takeaway
A function pointer is just an address — calling a NULL pointer is a jump to zero, not a function call.
Validate function pointers before calling them, especially in interrupt handlers or callback dispatch loops.
Use function pointers for runtime polymorphism in C, but prefer static dispatch when the target function is known at compile time.
Function Pointers in C — Embedded Firmware THECODEFORGE.IO Function Pointers in C — Embedded Firmware From declaration to state machines and C++ usage Function Pointer Declaration Type: return (*name)(params); address of code Callbacks Pass function ptr as argument to another Dispatch Tables Array of function ptrs replaces if-else Structs with Function Ptrs C's way to fake object-oriented behavior State Machines Clean event handling via function ptrs NULL Crash Trap Calling uninitialized or NULL function ptr ⚠ Calling a NULL function pointer crashes firmware Always check for NULL before calling or init to safe default THECODEFORGE.IO
thecodeforge.io
Function Pointers in C — Embedded Firmware
Function Pointers C

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.

function_pointer_basics.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>

/* 
 * io.thecodeforge naming convention applied to math operations 
 * All share the signature: (int, int) -> int
 */
int tcf_add(int a, int b)      { return a + b; }
int tcf_subtract(int a, int b) { return a - b; }
int tcf_multiply(int a, int b) { return a * b; }

// Typedef makes the syntax human-readable
typedef int (*MathOperation)(int, int);

int main(void) {
    // Pointer assignment (no & required, though valid)
    MathOperation operation = tcf_add;

    printf("tcf_add(10, 4)      = %d\n", operation(10, 4));

    operation = tcf_subtract;
    printf("tcf_subtract(10, 4) = %d\n", operation(10, 4));

    operation = tcf_multiply;
    printf("tcf_multiply(10, 4) = %d\n", operation(10, 4));

    return 0;
}
Output
tcf_add(10, 4) = 14
tcf_subtract(10, 4) = 6
tcf_multiply(10, 4) = 40
Pro Tip: Always typedef your function pointer types
Writing 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.
Production Insight
In production, the most common failure is assigning a function pointer without matching the signature.
The compiler does not check the signature unless you use typedef and -Werror.
Always use typedef and compile with -Werror=incompatible-pointer-types to catch mismatches at compile time.
Key Takeaway
Read function pointer declarations inside-out: the name with the asterisk is in the middle.
Typedef your function pointer types immediately — one line that prevents entire classes of bugs.
Call syntax is flexible: 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.

callback_pattern.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>

namespace io_thecodeforge {

typedef struct {
    char name[32];
    int  score;
} TcfPlayer;

// Comparator for qsort: descending order
int tcf_compare_descending(const void *a, const void *b) {
    const TcfPlayer *pa = (const TcfPlayer *)a;
    const TcfPlayer *pb = (const TcfPlayer *)b;
    return pb->score - pa->score;
}

// Predicate callback type
typedef int (*TcfPredicate)(const TcfPlayer *p);

void tcf_filter_players(TcfPlayer *list, int n, TcfPredicate should_print) {
    for (int i = 0; i < n; i++) {
        if (should_print(&list[i])) {
            printf("  %s: %d\n", list[i].name, list[i].score);
        }
    }
}

int tcf_is_pro(const TcfPlayer *p) { return p->score >= 90; }

}

int main(void) {
    using namespace io_thecodeforge;
    TcfPlayer team[] = {{"Alice", 95}, {"Bob", 45}, {"Charlie", 92}};
    int n = 3;

    qsort(team, n, sizeof(TcfPlayer), tcf_compare_descending);
    
    printf("Pro Players Only:\n");
    tcf_filter_players(team, n, tcf_is_pro);

    return 0;
}
Output
Pro Players Only:
Alice: 95
Charlie: 92
Interview Gold: Why does qsort take void* parameters?
qsort was written to sort any type — ints, structs, strings. Because C has no generics, it uses void to accept a pointer to anything. Your comparator receives void and must cast to the concrete type it expects. This is safe because you know what you put in the array — the type information lives with the caller, not with qsort.
Production Insight
Callbacks in production often cause issues when the callback pointer is invalidated (e.g., after module unload).
Always manage callback lifetimes: register and deregister pairs, and use weak references if possible.
A common production bug: passing a callback from a dynamically loaded library that gets unloaded, then calling the callback — immediate segfault.
Key Takeaway
qsort demonstrates the power of callbacks: algorithms become reusable.
Callback receivers must handle NULL pointers gracefully.
Callback providers must ensure the pointer remains valid — use registration/deregistration pairs.

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.

dispatch_table.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <string.h>

typedef void (*TcfHandler)(const char *arg);

void tcf_log_info(const char *msg)  { printf("[INFO] %s\n", msg); }
void tcf_log_error(const char *msg) { printf("[ERROR] %s\n", msg); }

typedef struct {
    const char *cmd;
    TcfHandler action;
} TcfRoute;

static const TcfRoute routes[] = {
    {"info",  tcf_log_info},
    {"error", tcf_log_error}
};

void tcf_dispatch(const char *cmd, const char *msg) {
    for (size_t i = 0; i < 2; i++) {
        if (strcmp(routes[i].cmd, cmd) == 0) {
            routes[i].action(msg);
            return;
        }
    }
    printf("Unknown command\n");
}

int main() {
    tcf_dispatch("info", "System start");
    tcf_dispatch("error", "Disk full");
    return 0;
}
Output
[INFO] System start
[ERROR] Disk full
Pro Tip: Dispatch tables scale where switch statements don't
A switch statement with 50 cases is unreadable. A dispatch table with 50 entries is just a list — you can sort it, load it from a config file, or build it at runtime. In performance-critical code, a table lookup is often faster than a long branch chain.
Production Insight
Dispatch tables are fast, but they are static. Changing behavior at runtime requires reloading the table.
In production, ensure thread safety when the table is updated concurrently — use a mutex or RCU.
A common mistake: forgetting to terminate the table with a sentinel, causing out-of-bounds access.
Key Takeaway
Dispatch tables are the C way of doing polymorphism — map an enum to a function.
They scale linearly with the number of cases, unlike switch statements.
Always validate the index before accessing the table to avoid out-of-bounds crashes.

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.

tcf_polymorphism.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

typedef struct TcfLogger TcfLogger;

struct TcfLogger {
    void (*write)(const char *msg);
};

void tcf_console_write(const char *msg) { printf("Console: %s\n", msg); }
void tcf_file_write(const char *msg)    { printf("File: %s\n", msg); }

int main() {
    TcfLogger console = { tcf_console_write };
    TcfLogger file    = { tcf_file_write };

    TcfLogger *loggers[] = { &console, &file };

    for(int i = 0; i < 2; i++) {
        loggers[i]->write("TCF Polymorphism Test");
    }

    return 0;
}
Output
Console: TCF Polymorphism Test
File: TCF Polymorphism Test
Interview Gold: This is exactly how C++ vtables work
When you declare a virtual function in C++, the compiler generates a vtable (dispatch table) and a vptr (hidden pointer) in every object. Calling a virtual function is just dereferencing that pointer. Knowing this explains the 'cost' of virtual functions: one extra pointer dereference.
Production Insight
Structs with function pointers are powerful but fragile — a missing initializer leaves the pointer unset.
Always provide an init function that fills in all function pointers, or use designated initializers.
In production, consider using a const vtable pointer shared across instances to save memory.
Key Takeaway
Function pointers in structs give you polymorphism without inheritance.
Always initialize all function pointers in a struct — incomplete initialization is a time bomb.
This pattern is used in Linux device drivers, embedded RTOS, and many firmware frameworks.

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.

state_machine_fp.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>

typedef struct TcfEvent TcfEvent;
struct TcfEvent {
    int type;
    int data;
};

// Forward declarations of state functions
typedef void (*TcfState)(const TcfEvent *event, TcfState *next);

void tcf_state_idle(const TcfEvent *event, TcfState *next) {
    printf("Idle state\n");
    if (event->type == 1) {
        *next = tcf_state_active;
    } else {
        *next = tcf_state_idle;
    }
}

void tcf_state_active(const TcfEvent *event, TcfState *next) {
    printf("Active state\n");
    if (event->type == 2) {
        *next = tcf_state_idle;
    } else {
        *next = tcf_state_active;
    }
}

int main() {
    TcfState current = tcf_state_idle;
    TcfEvent events[] = {{1, 0}, {1, 0}, {2, 0}, {1, 0}};
    for (int i = 0; i < 4; i++) {
        current(&events[i], &current);
    }
    return 0;
}
Output
Idle state
Active state
Active state
Idle state
Pro Tip: State machine with function pointers eliminates the switch
Each state is a separate function with full visibility into its own logic. Transitions are explicit and traceable. This pattern is standard in automotive and aerospace code where state machines are large and must be verified.
Production Insight
Function pointer state machines are fast but can be tricky to debug because the call stack doesn't show a state variable.
Use a debug hook that prints the current state function name on each transition.
Watch out for infinite loops if a state function forgets to update the next pointer — the machine hangs.
Key Takeaway
State machines implemented with function pointers are cleaner than giant switch statements.
Each state is a function, transitions are pointer assignments.
Always initialize the next state before returning — unassigned next leads to a stuck state machine.

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 multiply(), you're grabbing an address, not calling anything.

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.

AddressDemystified.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — c-cpp tutorial

#include <stdio.h>

void greet() {
    printf("Hello from greet!\n");
}

int main() {
    // Function name without parentheses = address
    printf("greet lives at: %p\n", greet);
    
    // Same address via explicit & operator
    printf("greet address:  %p\n", &greet);
    
    void (*fn)() = greet;
    fn();   // call via pointer
    (*fn)();// explicit dereference — both work

    return 0;
}
Output
greet lives at: 0x100003f4b0
greet address: 0x100003f4b0
Hello from greet!
Hello from greet!
Production Trap:
Never cast a function pointer to 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.
Key Takeaway
A function name without parentheses is just an address — treat it like any other pointer, because that's exactly what it is.

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.

CppMemberPtr.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — c-cpp tutorial

#include <iostream>

struct Server {
    void start() { std::cout << "Server started\n"; }
    void stop()  { std::cout << "Server stopped\n"; }
};

using Action = void(Server::*)();

int main() {
    Action action = &Server::start;  // note & required
    Server svr;
    (svr.*action)();  // dot-star operator

    action = &Server::stop;
    (svr.*action)();

    return 0;
}
Output
Server started
Server stopped
Senior Shortcut:
Use 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.
Key Takeaway
C++ member function pointers require the & 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 dlopen() on a shared object, the OS hands you a handle. You then use dlsym() 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 &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.

Plugin.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — c-cpp tutorial

#include <dlfcn.h>
#include <stdio.h>

typedef int (*plugin_init_t)(int version);

int main() {
    void* handle = dlopen("./simple_plugin.so", RTLD_NOW);
    if (!handle) { fprintf(stderr, "dlopen: %s\n", dlerror()); return 1; }

    plugin_init_t init = (plugin_init_t)dlsym(handle, "plugin_init");
    char* err = dlerror();
    if (err) { fprintf(stderr, "dlsym: %s\n", err); dlclose(handle); return 1; }

    int result = init(42);
    printf("Plugin returned: %d\n", result);
    dlclose(handle);
    return 0;
}
Output
Plugin returned: 1
Production Trap:
Never cast a function pointer through 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.
Key Takeaway
Function pointers let you invoke code that didn't exist at compile time. 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.

Device.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// io.thecodeforge — c-cpp tutorial

#include <stdint.h>
#include <stdio.h>

struct Device {
    uint8_t id;
    int (*write)(struct Device* self, uint8_t byte);
};

int uart_write(struct Device* self, uint8_t byte) {
    printf("UART(%d) wrote 0x%02X\n", self->id, byte);
    return 0;
}

int spi_write(struct Device* self, uint8_t byte) {
    printf("SPI(%d) wrote 0x%02X\n", self->id, byte);
    return 0;
}

int main() {
    struct Device uart = {.id = 1, .write = uart_write};
    struct Device spi  = {.id = 2, .write = spi_write};
    uart.write(&uart, 0xAA);
    spi.write(&spi, 0x55);
    return 0;
}
Output
UART(1) wrote 0xAA
SPI(2) wrote 0x55
Senior Shortcut:
Use a single function pointer per 'virtual method', not one per call site. Group them in a vtable struct and have each object point to one vtable. Saves 4 bytes per pointer times the number of methods — huge on 8-bit micros.
Key Takeaway
Structs with function pointers give you manual polymorphism with zero vtable overhead. Embedded systems use this instead of C++ because they control every byte of flash.
● Production incidentPOST-MORTEMseverity: high

NULL Function Pointer in Embedded Firmware

Symptom
Device crashes immediately when a specific task runs. No error message, just a reset. The crash log shows a hard fault at address 0x00000000.
Assumption
The function pointer was initialized at startup, but a code path skipped initialization under certain conditions.
Root cause
A conditional branch in the initialization code did not set the function pointer because a configuration flag was missing. The pointer remained NULL (zero). When the task dispatch attempted to call the function, the CPU jumped to address 0, causing a hard fault.
Fix
Initialize all function pointers to a safe default handler (a no-op function) at declaration. Add a NULL check before every call: if (handler) handler(); else default_handler();.
Key lesson
  • 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.
Production debug guideSymptom → Action guide for diagnosing function pointer failures in C code4 entries
Symptom · 01
Segfault when calling through a function pointer
Fix
Check if the pointer is NULL. In GDB: print ptr — if 0x0, look for missing initialization. Use a conditional breakpoint to catch the assignment.
Symptom · 02
Correct behavior sometimes, wrong behavior other times (non-deterministic)
Fix
Likely a dangling pointer to a function that went out of scope (e.g., function defined in another translation unit that was unloaded). Check that the function's address is still valid. In embedded: confirm the function resides in a non-overwritten code section.
Symptom · 03
Stack corruption after calling through a function pointer
Fix
The function pointer signature does not match the actual function. Check the number and types of arguments and return value. Use typedef and compile with -Werror to catch mismatches.
Symptom · 04
Program crashes only on release build, not debug
Fix
Optimizer may have inlined or removed the function pointer. Use volatile on the pointer or disable optimization for that file. Alternatively, check that the pointer is not optimized away.
★ Quick Debug Cheat Sheet for Function PointersWhen a function pointer call crashes, use these commands to diagnose the problem fast.
Segfault on function pointer call
Immediate action
Run GDB and inspect the pointer value.
Commands
print my_func_ptr
info functions my_func_ptr (to see if the target function exists in symbol table)
Fix now
Add a NULL check before the call: if (my_func_ptr) my_func_ptr();
Unexpected behavior, pointer seems valid+
Immediate action
Check the function signature at the call site vs the definition.
Commands
ptype my_func_ptr (in GDB to see the declared type)
whatis target_function (to see the actual function type)
Fix now
Ensure the signature matches exactly. Use typedef to enforce consistency.
Dangling pointer after module unload+
Immediate action
Verify the function is still loaded in memory.
Commands
info sharedlibrary (to see loaded shared objects)
disassemble target_function (to see if code is there)
Fix now
Deregister function pointers when unloading a module. Use a weak symbol or dynamic registration with a removal callback.
AspectFunction PointerDirect Function Call
FlexibilityRuntime decision — any matching functionCompile-time decision — fixed function
PerformanceOne extra pointer dereference (negligible)Inlineable — zero overhead possible
Syntax complexityRequires careful declaration and typedefStraightforward, no extra setup
Use caseCallbacks, plugins, dispatch tables, polymorphismKnown, fixed behavior — most code
DebuggabilityHarder — must inspect pointer value in debuggerEasy — function name visible in call stack
Null riskCalling NULL pointer = crash (undefined behavior)No null risk — call site is always resolved
TestabilityEasy to swap implementations in testsRequires wrapping or recompiling to mock

Key takeaways

1
The asterisk in a function pointer declaration belongs to the name, not the return type
int (op)(int, int) is a pointer to a function; int op(int, int) is a function that returns a pointer.
2
typedef your function pointer types immediately
it's one line of investment that makes every struct, parameter list, and return type that uses the type readable and maintainable.
3
A dispatch table (array of function pointers keyed by a command name or enum value) is the clean alternative to long switch or if-else chains
adding a new case means adding one table entry, not touching dispatcher logic.
4
Function pointers inside structs give you runtime polymorphism in C
the same mechanism the Linux kernel uses for device drivers and that C++ compilers use to implement virtual functions via vtables.
5
Always check for NULL before calling a function pointer
a single uninitialized pointer can crash your entire system.

Common mistakes to avoid

3 patterns
×

Missing parentheses around the pointer name in declaration

Symptom
Writing 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.
Fix
Always wrap (*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

Symptom
Uninitialized or reset pointers contain garbage or zero. Calling them causes a Segfault or HardFault immediately. In production, this is the number one source of crashes in code using function pointers.
Fix
Always initialize pointers to NULL and verify before execution: if (ptr) ptr(args);. Consider providing a default no-op function for empty states.
×

Signature mismatch when casting function pointers

Symptom
Forcing a 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.
Fix
Ensure the function signature exactly matches the typedef or the expected type. Never cast function pointers across incompatible signatures. Use compiler flags to enforce strict type checking.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between `void *f(int)` and `void (*f)(int)`?
Q02SENIOR
How would you implement a 'Plug-and-Play' driver architecture in C?
Q03SENIOR
Why is qsort's comparator function signature `int (*)(const void*, const...
Q04SENIOR
Explain the memory overhead and performance impact of using function poi...
Q01 of 04JUNIOR

What is the difference between `void *f(int)` and `void (*f)(int)`?

ANSWER
The first is a function declaration: 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).
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
How do I debug a function pointer that crashes?
02
Can I have an array of function pointers with different signatures?
03
Do I need the & operator to assign a function to a pointer?
04
What is the LeetCode equivalent for practicing function pointers?
05
How do I make function pointers thread-safe?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's C Basics. Mark it forged?

8 min read · try the examples if you haven't

Previous
typedef and enum in C
17 / 17 · C Basics
Next
Introduction to C++