Skip to content
Home C / C++ Function Pointers in C Explained — Syntax, Callbacks, and Real-World Patterns

Function Pointers in C Explained — Syntax, Callbacks, and Real-World Patterns

Where developers are forged. · Structured learning · Free forever.
📍 Part of: C Basics → Topic 17 of 17
Function pointers in C demystified: learn the syntax, why they exist, how callbacks work, and the real-world patterns senior devs use every day.
⚙️ Intermediate — basic C / C++ knowledge assumed
In this tutorial, you'll learn
Function pointers in C demystified: learn the syntax, why they exist, how callbacks work, and the real-world patterns senior devs use every day.
  • 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.
  • 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.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer

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.

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.c · C
123456789101112131415161718192021222324252627
#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.

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.c · C
1234567891011121314151617181920212223242526272829303132333435363738394041424344
#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.

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.c · C
123456789101112131415161718192021222324252627282930313233
#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.

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.c · C
1234567891011121314151617181920212223
#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.
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

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

⚠ Common Mistakes to Avoid

    Missing parentheses around the pointer name in the declaration — writing `int *handler(int)` instead of `int (*handler)(int)`. The first declares a function returning an int pointer; the second declares a pointer to a function. Fix: Always wrap `(*name)` to ensure the asterisk modifies the variable name, not the return type.
    Fix

    Always wrap (*name) to ensure the asterisk modifies the variable name, not the return type.

    Calling a NULL function pointer — uninitialized pointers contain garbage, and calling NULL results in a Segfault or HardFault. Fix: Always initialize pointers to NULL and verify before execution: `if (ptr) ptr(args);`.
    Fix

    Always initialize pointers to NULL and verify before execution: if (ptr) ptr(args);.

    Signature Mismatch Casting — trying to force a `void (*)(int)` function into a `void (*)(float)` pointer. Even if it compiles with a cast, the stack will be corrupted upon call. Fix: Ensure the function exactly matches the typedef's signature (parameters and return type).
    Fix

    Ensure the function exactly matches the typedef's signature (parameters and return type).

Interview Questions on This Topic

  • QQuestion: What is the difference between void f(int) and void (f)(int)? Answer: The first is a function returning a void pointer; the second is a pointer to a function returning void.
  • QQuestion: How would you implement a 'Plug-and-Play' driver architecture in C? Answer: Use a struct of function pointers (interface) and a registration function where drivers provide their specific implementations to a central dispatch table.
  • QQuestion: Why is qsort's comparator function signature int ()(const void, const void*)? Answer: Since C lacks generics, void pointers allow the function to handle any data type. The comparator then casts the pointers back to the relevant type for comparison.
  • QQuestion: Explain the memory overhead and performance impact of using function pointers for a state machine. Answer: Memory overhead is the size of the pointer (usually 4-8 bytes). Performance impact includes one extra dereference and the prevention of compiler inlining.

Frequently Asked Questions

How do I debug a function pointer that crashes?

Use a debugger like GDB to inspect the value of the pointer before the call. If the address is 0x0, it's a NULL pointer. If it's a random high address, it's likely uninitialized. Always initialize your pointers and check them before calling to prevent these 'googable' production crashes.

Can I have an array of function pointers with different signatures?

No. All elements in a C array must be of the same type. To simulate different signatures, you would typically use a 'generic' signature (like void* arguments) or wrap the function pointers in a union/struct with a type tag.

Do I need the & operator to assign a function to a pointer?

Technically no. In C, a function name 'decays' to a pointer in an expression, much like an array name. p = my_func; and p = &my_func; are functionally identical, though the latter is more explicit.

What is the LeetCode equivalent for practicing function pointers?

Problems involving custom sorting (using qsort or std::sort in C++) or implementing designs like a 'Min Stack' or 'LRU Cache' in C often require managing pointers to behavior. Implementing a 'Command Pattern' from scratch in C is also a standard interview challenge.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← Previoustypedef and enum in C
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged