Topic 57 of 100 Functions in C Interactive Tutorial

Function Pointers and Callbacks in C: How Callback Execution Works

14 mins read Last updated: 22 Aug 2026 32,100 views
C Compiler 0.00%

What is Function Pointers and Callbacks in C? Core Concept & Direct Summary

Core Objective & Problem Statement

How do function pointers store memory addresses of executable code and implement runtime callback patterns like `qsort()`?

Direct Answer Summary

Every pointer we've discussed so far has pointed at data — an int, a char, an array. Here's the idea that breaks most people's brains the first time: in C, functions also live in memory, at specific addresses — which means you can have a pointer that stores a function's location, and call the function through that pointer.

Why on earth would you want to? Because it enables callbacks — the ability to pass a function as an argument to another function, so that function can call yours back when the time is right. It's the mechanism behind sorting with custom rules, event handling, plug-in architectures, and generally half of what makes C libraries flexible.

This is advanced territory. We'll go slowly, build up from "a function has an address" to a working callback, and finish with the two patterns where function pointers genuinely earn their reputation.

(Suggested image: the hero diagram — a function's code sitting in a labeled memory box, a pointer variable pointing to that box, and an arrow calling through the pointer. Get this visual right and the rest of the page clicks.)


How Does Function Pointers and Callbacks in C Work? In-Depth Mechanics & Architecture

Step 1: A Function Has an Address

First, prove it to yourself. The name of a function, used on its own, gives you its address — just like the name of an array:

main.c
Run Online
#include <stdio.h>
void sayHello(void) {
    puts("Hello!");
}
int main() {
    printf("Function address: %p\n",(void *)sayHello);
    return 0;
}

Output (address will vary):

sayHello — no parentheses — is a pointer to the function. Add the parentheses (sayHello()) and you call it instead. That single distinction — name alone = address, name with () = call — is the seed of everything else on this page.


Step 2: Declaring a Function Pointer

A function pointer's declaration has to describe the function it can point to — return type, and parameter types:

main.c
Run Online
void(*ptr)(void);
// points to a void function taking no argsint (*calc)(int, int);
// points to an int function taking two ints

Read it right-to-left from the variable name: "ptr is a pointer to a function taking void, returning void."

The parentheses around *ptr are mandatory. Compare:

main.c
Run Online
int(*calc)(int, int);
// ✓ function POINTER — variables are separateint *calc(int, int);
// ✗ a function returning int* — totally different!

Drop the parentheses and the meaning flips completely. This is the most common function-pointer typo in existence, and now you know what to scan for.

(Suggested image: an annotated diagram with arrows from each part of int (*calc)(int, int); to its meaning)


Step 3: Assigning and Calling

main.c
Run Online
#include <stdio.h>
int add(int a, int b) {
    return a + b;
}
int mul(int a, int b) {
    return a * b;
}
int main() {
    int(*operation)(int, int);
    // declare the pointer
    operation = add;
    // point it at add    printf("add: %d\n", operation(5, 3));
    operation = mul;
    // now point it at mul    printf("mul: %d\n", operation(5, 3));
    return 0;
}

Output:

Two things to notice:

  1. Assigning needs no & — operation = add; works, because a function name is its address. (Writing operation = &add; also works, identically — both styles are accepted.)
  2. Calling through the pointer looks like calling a normal function — operation(5, 3). You can also write the more explicit (*operation)(5, 3), which is exactly equivalent. Modern style prefers omitting it.

The payoff is already visible: one pointer variable, two completely different behaviors, decided at runtime. That flexibility is what makes callbacks possible.


Step 4: Passing a Function as an Argument (The Baby Callback)

Now the real thing. A function can accept a function pointer as a parameter — that's a callback:

main.c
Run Online
#include <stdio.h>
void greetEnglish(void) {
    puts("Hello!");
}
void greetSpanish(void) {
    puts("Hola!");
}
void greetJapanese(void) {
    puts("Konnichiwa!");
}
void welcome(void(*greeting)(void)) {
    // ← accepts a function as a parameter    puts("--- Welcome ---");    greeting();                            // ← CALLBACK: calls whatever was passed in    puts("---------------");}
    int main() {
        welcome(greetEnglish);
        welcome(greetSpanish);
        welcome(greetJapanese);
        return 0;
    }

Output:

main.c
Run Online
--- Welcome ---Hello!------------------ Welcome ---Hola!------------------ Welcome ---Konnichiwa!---------------

Here's the key to understanding this: welcome doesn't know — or care — which greeting function it will receive. It receives an address, and at the marked line, it jumps to whatever code lives there. That jump is the callback.

Why this matters: before, the only way welcome could produce different behavior was to change its own code. Now behavior is a parameter. You can add a French greeting tomorrow without touching welcome at all — and that's precisely what libraries exploit: their code stays fixed while you supply the behavior.

(Suggested image: THE callback flow diagram — main() passing greetSpanish's address to welcome(), then welcome() jumping through the pointer into greetSpanish's code box and returning. This is the diagram readers need most.)


The Real-World Callback: qsort()

The standard library's qsort() is the canonical callback example — it can sort any array of anything, and it asks you which "smaller than" rule to use:

main.c
Run Online
void qsort(void *base, size_t count, size_t size, int(*compare)(const void *, const void *));

That last parameter is a callback. You write the comparison; qsort does the sorting and calls your function whenever it needs to decide the order of two elements.

Sorting numbers ascending:

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
int ascending(const void *a, const void *b) {
    int x = *(const int *)a;
    // cast void* back to int*    int y = *(const int *)b;    return x - y;                // negative: a first · zero: equal · positive: b first}
    int main() {
        int nums[] = {
            42, 7, 19, 88, 3, 55
        };
        int n = 6;
        qsort(nums, n, sizeof(int), ascending);
        for (int i = 0; i < n; i++) printf("%d ", nums[i]);
        return 0;
    }

Output:

Change one line — get descending order:

main.c
Run Online
int descending(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    return y - x;
    // swapped subtraction
}
qsort(nums, n, sizeof(int), descending);
// Output: 88 55 42 19 7 3

Same qsort code, same array, different callback — different ordering. That's the callback pattern delivering real, practical value. And notice the comparison function's contract: return negative if the first item should come first, zero if they're equal, positive if the second should come first. (The void * parameters are C's "any type" pointers — you cast them to the real type inside; they're a pointers-cluster topic.)

(Suggested image: qsort with two callback arrows — "ascending" and "descending" — both feeding into the same qsort box)


Making It Readable: typedef for Function Pointers

Raw function pointer declarations get ugly fast. The typedef shortcut fixes that:

main.c
Run Online
typedef int(*CompareFunc)(const void *, const void *);
typedef void(*GreetFunc)(void);
// Now the declarations are readable:void welcome(GreetFunc greeting);
void qsort(void *base, size_t count, size_t size, CompareFunc compare);

And they clarify what belongs in your array of function pointers (coming next):

main.c
Run Online
CompareFunc comparators[2] = {
    ascending, descending
}
;

Professional habit worth adopting immediately: if a function pointer type appears more than once, typedef it. int (*)(const void *, const void *) scattered three times in a file is hostile to readers.


The Menu Dispatch Pattern: An Array of Function Pointers

Remember the menu programs from our control flow cluster — switch statements mapping choices to actions? Function pointer arrays can do that job more elegantly, and they're a widely used real-world pattern:

main.c
Run Online
#include <stdio.h>
void newGame(void) {
    puts("Starting a new game...");
}
void loadGame(void) {
    puts("Loading your save...");
}
void settings(void) {
    puts("Opening settings...");
}
void quit(void) {
    puts("Goodbye!");
}
int main() {
    void(*menu[4])(void) = {
        newGame, loadGame, settings, quit
    };
    int choice;
    do {
        puts("\n1. New Game");
        puts("2. Load Game");
        puts("3. Settings");
        puts("4. Quit");
        printf("Choice: ");
        scanf("%d", &choice);
        if (choice >= 1 && choice <= 4) {
            menu[choice - 1]();
            // call the function at that index        } else {            puts("Invalid choice.");        }    } while (choice != 4);
            return 0;
        }

Sample Run:

main.c
Run Online
1. New Game2. Load Game3. Settings4. QuitChoice: 2Loading your save...

One line — menu[choice - 1]() — replaces the entire switch. Each array slot holds the address of one function, and indexing into it with the user's choice calls the matching one. Add a fifth menu option, and you add one function plus one array entry: done. No case labels to keep in sync, no break statements to forget.

This is exactly the kind of table-driven design you'll see in game engines, command processors, and device drivers.


Where Callbacks Live in the Real World

You're already using callbacks constantly, even if you didn't know:

  • qsort() / bsearch() — custom comparison rules
  • signal() — the operating system calls your function when Ctrl+C is pressed
  • GUI libraries — "when this button is clicked, run my handler function"
  • Thread and async libraries — "when this task finishes, call this function"
  • Event loops, network servers, embedded interrupts — all callback-driven

Every one of them is the same mechanism you just learned: store an address, jump to it later.


Common Function Pointer Mistakes

  1. Dropping the parentheses in the declaration — int *fn(int) declares a function returning an int pointer, not a function pointer. The (*name) grouping is mandatory.
  2. Calling a NULL or uninitialized function pointer — an unassigned function pointer points somewhere random; calling it crashes. Initialize to NULL and check before calling:
    main.c
    Run Online
    if (callback != NULL) callback();
  3. Signature mismatch — assigning a function with a different return or parameter type produces a warning and undefined behavior at the call. The callback can never have a different shape than the pointer expects (which is why qsort forces the void * contract).
  4. Forgetting & vs no-& confusion — both = add and = &add work, but pick one style and be consistent; mixing confuses readers.
  5. Overusing callbacks — for two options, a simple if is clearer than a function pointer. Callbacks earn their place when there are many interchangeable behaviors, or when someone else (a library, a user of your code) supplies the behavior.
  6. Ignoring the typedef and writing the raw pointer type repeatedly — technically fine, practically unreadable. typedef early.

(Suggested image: "6 Function Pointer Mistakes" checklist graphic, consistent with the series)


A Complete Example: A Mini Calculator With a Dispatch Table

Everything on this page, working together:

main.c
Run Online
#include <stdio.h>
typedef float(*Operation)(float, float);
float add(float a, float b) {
    return a + b;
}
float sub(float a, float b) {
    return a - b;
}
float mul(float a, float b) {
    return a * b;
}
float divi(float a, float b) {
    return(b != 0) ? a / b : 0;
}
float applyOperation(float x, float y, Operation op) {
    // callback parameter    return op(x, y);}
    int main() {
        Operation table[4] = {
            add, sub, mul, divi
        };
        char *names[4] = {
            "add", "subtract", "multiply", "divide"
        };
        float a = 12, b = 4;
        for (int i = 0; i < 4; i++) {
            printf("%-9s: %.2f\n", names[i], applyOperation(a, b, table[i]));
        }
        return 0;
    }

Output:

main.c
Run Online
add : 16.00subtract : 8.00multiply : 48.00divide : 3.00

A typedef makes the type readable, an array holds four interchangeable behaviors, and applyOperation receives any of them as a callback parameter. That's the complete pattern — and it's only a few lines.


Final Thoughts

Function pointers feel alien because we're used to functions being syntax — things you write and call directly. But in C, they're also data: addresses you can store, pass, compare, and choose between at runtime. That single reframe unlocks callbacks, and callbacks unlock the design style that most professional C code is built on.

Don't worry if this needs two or three passes. The path in is steady: understand that a function has an address → declare a pointer with matching shape → assign and call it → pass it to another function → that's a callback. Then borrow qsort's comparison pattern, and you're using the technique for real.

A heads-up for the series: this page leans on void * and casts, which are pointers-cluster topics. If any of that felt shaky, the Pointers cluster will fill the gaps properly — and this page will look obvious in retrospect.


Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
// Comparator callback for qsort
int compareAscending(const void *a, const void *b) {
    return(*(int*)a - *(int*)b);
}
int main() {
    int arr[] = {
        45, 12, 85, 32, 89, 5
    };
    int n = sizeof(arr) / sizeof(arr[0]);
    // Pass function pointer callback to qsort
    qsort(arr, n, sizeof(int), compareAscending);
    printf("Sorted array via qsort callback:\
");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\
");
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Always use typedef to define clean aliases for complex function pointer signatures to avoid unreadable code.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: How do you declare a `typedef` for a function pointer taking two `int`s and returning `int`?

A: `typedef int (*MathOp)(int, int);`
Q

Q: Where are function pointers used in operating systems?

A: In device driver virtual tables (struct of function pointers: `open`, `read`, `write`, `close`) and interrupt vector tables.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations