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

In Plain English 🔥
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.
⚡ 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.

The cleanest long-term habit is to use a typedef to name your function pointer type. It costs you one line and saves you from writing the hairy syntax everywhere the type appears — especially in struct definitions and function parameters.

function_pointer_basics.c · C
1234567891011121314151617181920212223242526272829
#include <stdio.h>

// Three simple math operations — all share the same signature: (int, int) -> int
int add(int a, int b)      { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }

// typedef gives the function pointer type a readable name.
// Read as: MathOperation is a pointer to a function taking two ints, returning int.
typedef int (*MathOperation)(int, int);

int main(void) {
    // Declare a function pointer and assign it — no & needed, function name decays to its address
    MathOperation operation = add;

    printf("add(10, 4)      = %d\n", operation(10, 4));  // calls add via pointer

    operation = subtract;                                  // reassign — now points to subtract
    printf("subtract(10, 4) = %d\n", operation(10, 4));

    operation = multiply;                                  // reassign again
    printf("multiply(10, 4) = %d\n", operation(10, 4));

    // You can also use the explicit dereference form — identical behaviour
    MathOperation explicit_op = add;
    printf("(*explicit_op)(10, 4) = %d\n", (*explicit_op)(10, 4));

    return 0;
}
▶ Output
add(10, 4) = 14
subtract(10, 4) = 6
multiply(10, 4) = 40
(*explicit_op)(10, 4) = 14
⚠️
Pro Tip: Always typedef your function pointer typesWriting `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. That separation of concerns is exactly why callbacks are powerful.

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.

This is also how GUI toolkits register button-click handlers, how operating systems deliver timer signals, and how networking libraries notify your code when data arrives. Once you recognize the pattern, you'll see it everywhere.

callback_pattern.c · C
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// --- Part 1: Building a custom qsort comparator ---

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

// Comparator for ascending score order.
// qsort passes void* pointers — we cast them to the type we know they are.
int compare_score_ascending(const void *a, const void *b) {
    const Player *player_a = (const Player *)a;
    const Player *player_b = (const Player *)b;
    return player_a->score - player_b->score;  // negative = a comes first
}

// Comparator for descending score order — just flip the subtraction
int compare_score_descending(const void *a, const void *b) {
    const Player *player_a = (const Player *)a;
    const Player *player_b = (const Player *)b;
    return player_b->score - player_a->score;
}

// --- Part 2: Our own callback-based filter function ---

typedef int (*PlayerPredicate)(const Player *player);

// Calls predicate on every player; prints those for which predicate returns non-zero.
// The caller decides the filter rule — this function stays generic.
void print_filtered_players(const Player *players, int count, PlayerPredicate predicate) {
    for (int i = 0; i < count; i++) {
        if (predicate(&players[i])) {           // callback invoked here
            printf("  %-15s %d\n", players[i].name, players[i].score);
        }
    }
}

// Two different filter predicates — same signature, different logic
int is_high_scorer(const Player *player) { return player->score >= 80; }
int is_low_scorer(const Player *player)  { return player->score < 50; }

int main(void) {
    Player players[] = {
        {"Alice",   92},
        {"Bob",     45},
        {"Charlie", 78},
        {"Diana",   33},
        {"Eve",     88}
    };
    int count = sizeof(players) / sizeof(players[0]);

    // Sort ascending using qsort + our comparator callback
    qsort(players, count, sizeof(Player), compare_score_ascending);
    printf("Sorted ascending by score:\n");
    for (int i = 0; i < count; i++) {
        printf("  %-15s %d\n", players[i].name, players[i].score);
    }

    // Sort descending — swap the callback, everything else stays the same
    qsort(players, count, sizeof(Player), compare_score_descending);
    printf("\nSorted descending by score:\n");
    for (int i = 0; i < count; i++) {
        printf("  %-15s %d\n", players[i].name, players[i].score);
    }

    // Use our custom filter with two different predicate callbacks
    printf("\nHigh scorers (>= 80):\n");
    print_filtered_players(players, count, is_high_scorer);  // passes predicate

    printf("\nLow scorers (< 50):\n");
    print_filtered_players(players, count, is_low_scorer);   // different predicate, same function

    return 0;
}
▶ Output
Sorted ascending by score:
Diana 33
Bob 45
Charlie 78
Alice 92
Eve 88

Sorted descending by score:
Alice 92
Eve 88
Charlie 78
Bob 45
Diana 33

High scorers (>= 80):
Alice 92
Eve 88

Low scorers (< 50):
Bob 45
Diana 33
🔥
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. Interviewers love asking this because it tests your understanding of void pointers AND callbacks at once.

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 maintenance headache and a violation of the open-closed principle even in C.

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.

This pattern is how CPU emulators dispatch opcodes, how HTTP servers route methods, and how state machines transition between states. The Linux kernel uses tables of function pointers (called file_operations, net_proto_ops, etc.) to implement its driver interface — every device driver is essentially a struct full of function pointers.

dispatch_table.c · C
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
#include <stdio.h>
#include <string.h>

// --- A tiny command dispatcher using a dispatch table ---

// All command handler functions share this signature
typedef void (*CommandHandler)(const char *argument);

// The actual command implementations
void handle_hello(const char *argument) {
    printf("Hello, %s!\n", argument[0] ? argument : "world");
}

void handle_shout(const char *argument) {
    // Print the argument in uppercase-style by prepending SHOUT:
    printf("SHOUT: ");
    for (int i = 0; argument[i] != '\0'; i++) {
        // Convert lowercase letters to uppercase manually
        char c = argument[i];
        if (c >= 'a' && c <= 'z') c -= 32;
        putchar(c);
    }
    putchar('\n');
}

void handle_count(const char *argument) {
    int length = 0;
    while (argument[length] != '\0') length++;
    printf("'%s' has %d characters.\n", argument, length);
}

void handle_reverse(const char *argument) {
    int length = 0;
    while (argument[length] != '\0') length++;
    printf("Reversed: ");
    for (int i = length - 1; i >= 0; i--) putchar(argument[i]);
    putchar('\n');
}

// --- The dispatch table itself ---
// A struct pairs a command name with its handler — clean and extensible.
typedef struct {
    const char    *command_name;
    CommandHandler handler;          // function pointer stored inside a struct
} DispatchEntry;

// Adding a new command = adding one row here. The dispatcher never changes.
static const DispatchEntry dispatch_table[] = {
    { "hello",   handle_hello   },
    { "shout",   handle_shout   },
    { "count",   handle_count   },
    { "reverse", handle_reverse },
};

// Calculate table size at compile time — avoids magic numbers
static const int TABLE_SIZE = sizeof(dispatch_table) / sizeof(dispatch_table[0]);

// The dispatcher: walks the table, finds the matching entry, calls the handler.
void dispatch(const char *command, const char *argument) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        if (strcmp(dispatch_table[i].command_name, command) == 0) {
            dispatch_table[i].handler(argument);  // function pointer call
            return;
        }
    }
    printf("Unknown command: '%s'\n", command);
}

int main(void) {
    dispatch("hello",   "Alice");
    dispatch("shout",   "make some noise");
    dispatch("count",   "function pointers");
    dispatch("reverse", "dispatch");
    dispatch("quit",    "");   // not in the table — triggers unknown-command path

    return 0;
}
▶ Output
Hello, Alice!
SHOUT: MAKE SOME NOISE
'function pointers' has 17 characters.
Reversed: hctapsid
Unknown command: 'quit'
⚠️
Pro Tip: Dispatch tables scale where switch statements don'tA 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 (emulators, parsers), a table lookup is also typically faster than a long chain of branch comparisons because modern CPUs handle table-based indirect calls more predictably.

Function Pointers in Structs — How C Fakes Object-Oriented Design

Here's where function pointers go from useful to genuinely powerful. 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, and it's how the C standard library and the Linux kernel achieve polymorphism without C++.

The pattern works like this: you define a struct that holds both the data fields and function pointer fields. Different 'instances' can point their function pointers at different implementations. Code that operates on the struct calls the function through the pointer without knowing which implementation it's talking to. That's runtime polymorphism in plain C.

This matters in practice because C is used in environments where C++ isn't an option — embedded microcontrollers, kernel modules, real-time systems. Knowing how to build a clean interface with function pointers lets you write genuinely reusable, extensible C code rather than copying and modifying logic for every new variant.

The pattern also teaches you something important about C++: when you understand that a vtable is just a compiler-generated dispatch table of function pointers attached to every object through a hidden pointer, virtual functions stop feeling like magic.

oop_in_c.c · C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
#include <stdio.h>
#include <math.h>

// --- Runtime polymorphism in C using function pointers in structs ---

// Forward declaration — Shape needs to refer to itself
typedef struct Shape Shape;

// A 'virtual function table': a struct of function pointers.
// Every Shape type provides its own implementation of these.
typedef struct {
    double (*area)(const Shape *self);      // how to compute this shape's area
    double (*perimeter)(const Shape *self); // how to compute its perimeter
    void   (*describe)(const Shape *self);  // how to print a description
} ShapeOps;

// The base Shape struct: holds a pointer to its ops table and shared data.
struct Shape {
    const ShapeOps *ops;   // pointer to the function table — this IS the polymorphism
    const char     *label; // a human-readable name for printing
};

// --- Circle implementation ---
typedef struct {
    Shape  base;   // MUST be first — allows safe casting between Shape* and Circle*
    double radius;
} Circle;

double circle_area(const Shape *self) {
    const Circle *circle = (const Circle *)self;  // safe: base is first member
    return M_PI * circle->radius * circle->radius;
}
double circle_perimeter(const Shape *self) {
    const Circle *circle = (const Circle *)self;
    return 2.0 * M_PI * circle->radius;
}
void circle_describe(const Shape *self) {
    const Circle *circle = (const Circle *)self;
    printf("Circle (radius=%.2f)\n", circle->radius);
}

// The ops table for circles — one shared instance for all circles
static const ShapeOps CIRCLE_OPS = {
    circle_area, circle_perimeter, circle_describe
};

void circle_init(Circle *circle, double radius) {
    circle->base.ops   = &CIRCLE_OPS;  // wire up the function table
    circle->base.label = "circle";
    circle->radius     = radius;
}

// --- Rectangle implementation ---
typedef struct {
    Shape  base;
    double width;
    double height;
} Rectangle;

double rect_area(const Shape *self) {
    const Rectangle *rect = (const Rectangle *)self;
    return rect->width * rect->height;
}
double rect_perimeter(const Shape *self) {
    const Rectangle *rect = (const Rectangle *)self;
    return 2.0 * (rect->width + rect->height);
}
void rect_describe(const Shape *self) {
    const Rectangle *rect = (const Rectangle *)self;
    printf("Rectangle (%.2f x %.2f)\n", rect->width, rect->height);
}

static const ShapeOps RECT_OPS = {
    rect_area, rect_perimeter, rect_describe
};

void rect_init(Rectangle *rect, double width, double height) {
    rect->base.ops   = &RECT_OPS;
    rect->base.label = "rectangle";
    rect->width      = width;
    rect->height     = height;
}

// --- Generic function that works on ANY Shape ---
// It never knows if it has a circle or rectangle — it just calls through the ops pointer.
void print_shape_stats(const Shape *shape) {
    shape->ops->describe(shape);                         // polymorphic call
    printf("  Area:      %.4f\n", shape->ops->area(shape));
    printf("  Perimeter: %.4f\n", shape->ops->perimeter(shape));
}

int main(void) {
    Circle    c;  rect_init(NULL, 0, 0);  // just declarations
    Rectangle r;

    circle_init(&c, 5.0);
    rect_init(&r, 4.0, 6.0);

    // An array of base Shape pointers — holds mixed types
    const Shape *shapes[] = { (Shape *)&c, (Shape *)&r };
    int shape_count = sizeof(shapes) / sizeof(shapes[0]);

    for (int i = 0; i < shape_count; i++) {
        print_shape_stats(shapes[i]);  // same call, different behavior
        printf("\n");
    }

    return 0;
}
▶ Output
Circle (radius=5.00)
Area: 78.5398
Perimeter: 31.4159

Rectangle (4.00 x 6.00)
Area: 24.0000
Perimeter: 20.0000
🔥
Interview Gold: This is exactly how C++ vtables workWhen you declare a virtual function in C++, the compiler generates a table of function pointers (the vtable) and adds a hidden pointer to it (the vptr) in every object. Calling a virtual function is just dereferencing that pointer and jumping to the right entry. Knowing this lets you answer 'what is the cost of a virtual function call?' (one extra pointer dereference) — a question that separates strong C++ candidates from the rest.
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. Get this backwards and you get a confusing compiler error.
  • 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

  • Mistake 1: Missing parentheses around the pointer name in the declaration — writing int handler(int) instead of int (handler)(int) — the first declares a function that returns int, not a pointer to a function. Your compiler will catch this but the error message is confusing. Fix: always wrap the asterisk and name together in parentheses, then write the parameter list outside: int (handler)(int).
  • Mistake 2: Calling a NULL function pointer — if a function pointer is uninitialized or set to NULL and you call it, you get undefined behavior (typically a segfault). This is silent and devastating in embedded systems where you can't always attach a debugger. Fix: add a NULL guard before every call through a function pointer you didn't personally initialize: if (handler != NULL) { handler(arg); }.
  • Mistake 3: Casting a function pointer to an incompatible signature and calling it — for example, casting a void ()(int) to void ()(int, int) to 'add a parameter.' This compiles without warnings if you're explicit about the cast, but it's undefined behavior. The calling convention mismatch corrupts the stack. Fix: every function you assign to a pointer must exactly match the declared signature — return type, parameter count, and parameter types all included. If you need flexibility, use void* parameters with a documented convention (the qsort model) rather than casting.

Interview Questions on This Topic

  • QCan you explain the difference between `int *foo(int)` and `int (*foo)(int)` in C, and why does the distinction matter?
  • QHow would you implement a basic plugin system in C where new behaviors can be added without modifying the core dispatcher? Walk me through the data structures and function signatures you'd design.
  • QWhat is a vtable in C++, and how does its mechanism relate to what you'd build manually with function pointers in a C struct? What is the runtime cost of a virtual function call compared to a direct call?

Frequently Asked Questions

What is a function pointer in C and when should I use one?

A function pointer is a variable that holds the memory address of a function. Use one when you need to decide at runtime which function to call — for callbacks, custom sort comparators, event handlers, or dispatch tables. If you find yourself writing a long if-else chain that calls different functions based on a mode flag, a function pointer table is almost always the cleaner solution.

Do I need the ampersand (&) when assigning a function to a function pointer?

No. In C, a function name in an expression context automatically decays to a pointer to that function — exactly the same way an array name decays to a pointer to its first element. Writing handler = my_func and handler = &my_func are equivalent. Most C programmers omit the & to keep it concise, but both forms are valid.

Is calling a function through a pointer slower than calling it directly?

In practice, the difference is negligible for the vast majority of code — it's one extra memory dereference. The measurable cost comes indirectly: the compiler cannot inline a call through a function pointer the way it can inline a direct call, so you lose the compiler's ability to optimize across the call boundary. This matters in tight inner loops. For everything else — event handlers, dispatch tables, callbacks that run occasionally — the flexibility is worth far more than the tiny overhead.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousCustom Allocators in C++Next →Expression Templates in C++
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged