Module 5.4

Function Pointers

Unlock the power of treating functions as first-class data in C. Learn how to store function addresses in variables, pass functions as arguments, implement callbacks, and build flexible systems like event handlers and plugin architectures.

40 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Declare and initialize function pointers
  • Pass functions as arguments (callbacks)
  • Create arrays of function pointers
  • Use typedef for cleaner syntax
  • Build real-world applications (menus, sorting)
Contents
01

Function Pointer Basics

Just as you can have pointers to variables, you can have pointers to functions. A function pointer stores the memory address where a function's executable code begins. This allows you to call functions indirectly, pass them as arguments, and store them in data structures.

What is a Function Pointer?

When you compile a C program, each function is placed somewhere in memory. The function's name, when used without parentheses, evaluates to the address of that code. A function pointer is a variable that can hold this address and be used to call the function later.

Concept

Function Pointer

A function pointer is a variable that stores the address of a function. It allows you to call that function indirectly through the pointer, enabling dynamic function selection at runtime.

The declaration syntax specifies the return type and parameter types of functions that the pointer can point to. Only functions with matching signatures can be assigned.

Key insight: Function pointers enable "higher-order" programming in C, where functions can accept other functions as parameters or return them as results.

Declaring Function Pointers

The syntax for declaring a function pointer can look intimidating at first. The key is understanding that you are declaring a pointer (*) to a function with a specific signature. The parentheses around *pointer_name are essential because of operator precedence.

// Syntax: return_type (*pointer_name)(parameter_types);

// Pointer to a function that takes two ints and returns an int
int (*operation)(int, int);

// Pointer to a function that takes no args and returns void
void (*callback)(void);

// Pointer to a function that takes a char* and returns int
int (*parser)(char *);
Parentheses matter! Without them, int *operation(int, int) declares a function that returns int*, not a pointer to a function.

Initializing Function Pointers

To assign a function's address to a pointer, you can use the function name directly (it automatically converts to a pointer) or use the address-of operator &. Both approaches are equivalent and commonly used.

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    // Method 1: Direct assignment (function name decays to pointer)
    int (*op)(int, int) = add;
    
    // Method 2: Using address-of operator (explicit)
    int (*op2)(int, int) = &subtract;
    
    // Both work identically
    printf("%d\n", op(10, 5));   // 15
    printf("%d\n", op2(10, 5));  // 5
    
    return 0;
}

Calling Through Function Pointers

Once you have a function pointer, you can call the function it points to. There are two syntaxes for this: the dereferencing syntax (*ptr)(args) and the direct syntax ptr(args). The direct syntax is more commonly used because it is cleaner.

int multiply(int a, int b) {
    return a * b;
}

int main() {
    int (*calc)(int, int) = multiply;
    
    // Method 1: Explicit dereference (traditional)
    int result1 = (*calc)(6, 7);  // 42
    
    // Method 2: Direct call (preferred, cleaner)
    int result2 = calc(6, 7);     // 42
    
    printf("Results: %d, %d\n", result1, result2);
    return 0;
}
Why two syntaxes? The C standard allows both for convenience. The direct syntax ptr(args) is preferred in modern code because it reads more naturally and mirrors regular function calls.

Comparing Function Pointers

You can compare function pointers for equality to check if they point to the same function. This is useful for validation or when implementing dispatch tables.

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main() {
    int (*op1)(int, int) = add;
    int (*op2)(int, int) = add;
    int (*op3)(int, int) = sub;
    
    if (op1 == op2) {
        printf("op1 and op2 point to same function\n");  // Prints
    }
    
    if (op1 != op3) {
        printf("op1 and op3 point to different functions\n");  // Prints
    }
    
    // NULL check before calling
    if (op1 != NULL) {
        printf("Result: %d\n", op1(5, 3));  // 8
    }
    
    return 0;
}

Practice Questions: Function Pointer Basics

Task: Write a function divide that takes two doubles and returns their quotient. Create a function pointer to it and use the pointer to divide 20.0 by 4.0.

Show Solution
#include <stdio.h>

double divide(double a, double b) {
    return a / b;
}

int main() {
    double (*div_ptr)(double, double) = divide;
    
    double result = div_ptr(20.0, 4.0);
    printf("Result: %.2f\n", result);  // 5.00
    
    return 0;
}

Task: Create two functions greet_english and greet_spanish that print greetings. Use a function pointer to switch between them based on a variable.

Show Solution
#include <stdio.h>

void greet_english(void) {
    printf("Hello, World!\n");
}

void greet_spanish(void) {
    printf("Hola, Mundo!\n");
}

int main() {
    void (*greet)(void);
    int use_spanish = 1;
    
    if (use_spanish) {
        greet = greet_spanish;
    } else {
        greet = greet_english;
    }
    
    greet();  // Hola, Mundo!
    
    return 0;
}

Task: Write a function get_operation that takes a char ('+', '-', '*') and returns a pointer to the appropriate math function.

Show Solution
#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

int (*get_operation(char op))(int, int) {
    switch (op) {
        case '+': return add;
        case '-': return sub;
        case '*': return mul;
        default:  return NULL;
    }
}

int main() {
    int (*operation)(int, int);
    
    operation = get_operation('+');
    printf("5 + 3 = %d\n", operation(5, 3));  // 8
    
    operation = get_operation('*');
    printf("5 * 3 = %d\n", operation(5, 3));  // 15
    
    return 0;
}
02

Callback Functions

A callback is a function passed as an argument to another function. The receiving function can then "call back" to the passed function at the appropriate time. This pattern is fundamental to event-driven programming, sorting algorithms, and modular design.

Understanding Callbacks

Callbacks allow you to customize behavior without modifying existing code. Instead of hardcoding what should happen, you pass a function that defines the behavior. The classic example is the C standard library's qsort() function.

Pattern

Callback Function

A callback is a function that you provide to another function, which then invokes it at a specific point during its execution. The calling function does not need to know the callback's implementation, only its signature.

Common uses: Custom sorting comparators, event handlers, asynchronous operations, plugin systems, and iterator patterns.

Creating a Callback Pattern

Let us build a simple example where a function processes an array and calls a user-provided callback for each element. This demonstrates the power of separating the iteration logic from the processing logic. We will build this step by step.

First, decide what your callback function should look like. For processing array elements, we need a function that takes a single integer and does something with it. The return type is void since we just want to perform an action. This signature becomes the contract that all callback functions must follow. Any function that matches this signature can be passed as a callback, giving you flexibility to define different behaviors without changing the core iteration logic.

// Callback signature: takes an int, returns nothing
// void (*callback)(int)

// Example callbacks that match this signature:
void print_value(int x) {
    printf("%d ", x);
}

void print_squared(int x) {
    printf("%d ", x * x);
}

Now create a function that accepts a callback as a parameter. This function handles the iteration logic, while the callback handles what to do with each element. Notice how the third parameter is a function pointer that specifies the expected callback signature. The function does not need to know what the callback does internally. It simply calls the provided function for each element in the array, completely decoupling the traversal logic from the processing logic.

// Higher-order function that accepts a callback
void for_each(int *arr, int size, void (*callback)(int)) {
    for (int i = 0; i < size; i++) {
        callback(arr[i]);  // Invoke callback for each element
    }
}
Why is this powerful? The for_each function does not know or care what the callback does. It only knows how to iterate. This separation of concerns makes code reusable and flexible.

Create several callback functions that match the expected signature. Each one performs a different operation on the element it receives. Notice that all these functions have the same signature: they take one integer parameter and return void. This uniformity is what allows them to be used interchangeably with our for_each function. You can add as many callback functions as you need without modifying the iteration logic at all.

void print_value(int x) {
    printf("%d ", x);
}

void print_squared(int x) {
    printf("%d ", x * x);
}

void print_doubled(int x) {
    printf("%d ", x * 2);
}

void print_negative(int x) {
    printf("%d ", -x);
}

Now put it all together. Pass different callbacks to achieve different behaviors without changing the for_each function. The same array and the same iteration function produce completely different outputs depending on which callback you provide. This is the essence of the callback pattern: write the logic once, and customize behavior by swapping functions. This approach is used extensively in libraries and frameworks to provide extensibility points.

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int size = 5;
    
    printf("Original: ");
    for_each(numbers, size, print_value);
    printf("\n");  // 1 2 3 4 5
    
    printf("Squared:  ");
    for_each(numbers, size, print_squared);
    printf("\n");  // 1 4 9 16 25
    
    return 0;
}

Here is the complete program that demonstrates the callback pattern with multiple different transformations on the same data. When you run this program, you will see the same array processed three different ways. Each call to for_each uses the same iteration logic but produces different output based on the callback provided. This pattern forms the foundation of functional programming concepts in C.

#include <stdio.h>

// Callbacks
void print_value(int x)   { printf("%d ", x); }
void print_squared(int x) { printf("%d ", x * x); }
void print_doubled(int x) { printf("%d ", x * 2); }

// Higher-order function
void for_each(int *arr, int size, void (*callback)(int)) {
    for (int i = 0; i < size; i++) {
        callback(arr[i]);
    }
}

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    
    printf("Values:  "); for_each(nums, 5, print_value);   printf("\n");
    printf("Squared: "); for_each(nums, 5, print_squared); printf("\n");
    printf("Doubled: "); for_each(nums, 5, print_doubled); printf("\n");
    
    return 0;
}

Output:

Values:  1 2 3 4 5 
Squared: 1 4 9 16 25 
Doubled: 2 4 6 8 10

Let us create a more advanced version that accumulates results. This pattern is useful for summing, finding max/min, or building new arrays. Unlike the previous example where callbacks just printed values, here the callback returns a transformed value that gets accumulated. This shows how callbacks can be used to not just perform actions but also to transform data in a pipeline-style processing approach commonly used in functional programming.

#include <stdio.h>

// Callback that returns a transformed value
typedef int (*Transformer)(int);

int square(int x) { return x * x; }
int triple(int x) { return x * 3; }

// Apply transformation and sum results
int transform_and_sum(int *arr, int size, Transformer fn) {
    int total = 0;
    for (int i = 0; i < size; i++) {
        total += fn(arr[i]);
    }
    return total;
}

int main() {
    int nums[] = {1, 2, 3, 4};
    
    int sum_squares = transform_and_sum(nums, 4, square);
    printf("Sum of squares: %d\n", sum_squares);  // 30
    
    int sum_triples = transform_and_sum(nums, 4, triple);
    printf("Sum of triples: %d\n", sum_triples);  // 30
    
    return 0;
}
Key takeaway: The callback pattern separates "what to do" from "how to iterate." This makes your code more modular, testable, and reusable.

Using qsort() with Custom Comparators

The qsort() function from <stdlib.h> is the most common example of callbacks in C. It sorts any array if you provide a comparison function that knows how to compare two elements. Let us break down how it works. The beauty of qsort() is that it can sort arrays of any type, whether integers, floats, strings, or custom structures, as long as you provide the appropriate comparison logic through a callback function.

The qsort() function has this signature: void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)). The last parameter is a function pointer to your custom comparator. The use of void* for the base pointer allows qsort() to work with any data type. The size parameter tells it how many bytes to move when swapping elements, making it completely generic and type-agnostic.

// qsort signature breakdown:
// base   - pointer to array to sort
// nmemb  - number of elements
// size   - size of each element
// compar - comparison function pointer

#include <stdlib.h>
qsort(array, count, sizeof(element), compare_func);

A comparator function must follow a specific pattern. It receives two const void* pointers (generic pointers to any type) and returns an integer indicating the relative order. The const qualifier ensures that the comparator does not modify the elements being compared. Inside your comparator, you must cast these void pointers to the actual type of elements in your array before comparing them.

// Comparator return values:
//   negative: first element comes BEFORE second
//   zero:     elements are EQUAL
//   positive: first element comes AFTER second

int compare(const void *a, const void *b) {
    // Cast void pointers to your actual type
    // Compare and return result
}

For sorting integers in ascending order, cast the void pointers to int*, dereference them, and subtract. The subtraction naturally produces negative, zero, or positive results. If the first value is smaller, the result is negative, placing it before the second. If they are equal, zero is returned. If the first is larger, a positive result places it after the second in the sorted output.

// Ascending order comparator
int compare_asc(const void *a, const void *b) {
    int val1 = *(int *)a;  // Cast and dereference
    int val2 = *(int *)b;
    return val1 - val2;    // Negative if val1 < val2
}

For descending order, simply reverse the subtraction. This flips the sign of the result, reversing the sort order. By subtracting the first value from the second instead of the second from the first, larger values will now come before smaller values. This simple change in the comparator completely reverses the sorting behavior without any changes to the qsort() call itself.

// Descending order comparator
int compare_desc(const void *a, const void *b) {
    int val1 = *(int *)a;
    int val2 = *(int *)b;
    return val2 - val1;  // Reversed: positive if val1 < val2
}

Now use qsort() with your comparators. Pass the array, element count, element size, and the comparison function. The function name without parentheses serves as a pointer to the function. After the call completes, your array will be sorted in-place according to the order defined by your comparator. You can sort the same array multiple times with different comparators to achieve different orderings.

int nums[] = {64, 25, 12, 22, 11};
int n = 5;

// Sort ascending
qsort(nums, n, sizeof(int), compare_asc);
// Result: 11 12 22 25 64

// Sort descending
qsort(nums, n, sizeof(int), compare_desc);
// Result: 64 25 22 12 11

Here is the complete working example that demonstrates sorting an integer array in both ascending and descending order. The program first sorts the array in ascending order and prints the result, then sorts the same array in descending order. Notice how the same qsort() call produces completely different results based solely on which comparator function is passed as the final argument.

#include <stdio.h>
#include <stdlib.h>

int compare_asc(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);
}

int compare_desc(const void *a, const void *b) {
    return (*(int *)b - *(int *)a);
}

void print_array(int *arr, int n) {
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
}

int main() {
    int nums[] = {64, 25, 12, 22, 11};
    
    qsort(nums, 5, sizeof(int), compare_asc);
    print_array(nums, 5);  // 11 12 22 25 64
    
    qsort(nums, 5, sizeof(int), compare_desc);
    print_array(nums, 5);  // 64 25 22 12 11
    
    return 0;
}
Overflow warning: The subtraction trick a - b can overflow with very large integers. For production code, use explicit comparisons: return (a > b) - (a < b);

Sorting Strings and Structures

The same qsort() pattern works for any data type. The key is understanding how to cast the void pointers correctly for your specific type. Let us start with sorting strings.

When sorting an array of strings (char* pointers), qsort passes pointers to each element. Since each element is already a char*, you receive a char** (pointer to pointer).

// Array of string pointers
const char *names[] = {"Charlie", "Alice", "Bob"};

// qsort passes: pointer to each element
// Each element is char*, so you get char**

The string comparator must dereference the double pointer to get the actual string, then use strcmp() which already returns the correct negative/zero/positive values. The strcmp() function compares strings character by character and returns a negative value if the first string comes before the second alphabetically, zero if they are equal, and a positive value if the first string comes after the second.

int compare_strings(const void *a, const void *b) {
    // a and b are pointers to char* elements
    const char *str1 = *(const char **)a;  // Dereference to get char*
    const char *str2 = *(const char **)b;
    return strcmp(str1, str2);  // strcmp returns correct values
}

Here is the complete example for sorting an array of strings alphabetically. The program creates an array of string pointers, sorts them using qsort() with our custom comparator, and prints the result. After sorting, the strings appear in alphabetical order. This same technique works for sorting any array of pointers where you need custom comparison logic.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int compare_strings(const void *a, const void *b) {
    return strcmp(*(const char **)a, *(const char **)b);
}

int main() {
    const char *names[] = {"Charlie", "Alice", "Bob", "Diana"};
    
    qsort(names, 4, sizeof(char *), compare_strings);
    
    for (int i = 0; i < 4; i++) printf("%s ", names[i]);
    // Output: Alice Bob Charlie Diana
    return 0;
}

Sorting structures requires a similar approach. First, define your structure with the fields you want to sort by. A structure can have multiple fields, and you can create different comparators to sort by any of them. This gives you the flexibility to display the same data in different orders based on user preference or application requirements.

typedef struct {
    char name[50];
    int age;
    float salary;
} Employee;

For structures, qsort passes pointers directly to each struct element. Cast the void pointer to your struct pointer type, then access the field you want to compare. Unlike arrays of pointers where you get a pointer to a pointer, here you receive a direct pointer to each struct in the array. Use the arrow operator -> to access the specific field you want to compare.

// Comparator to sort by age (ascending)
int compare_by_age(const void *a, const void *b) {
    Employee *emp1 = (Employee *)a;  // Cast to struct pointer
    Employee *emp2 = (Employee *)b;
    return emp1->age - emp2->age;    // Compare age field
}

You can create multiple comparators for different fields. For string fields like name, use strcmp() on the struct members. For numeric fields like salary, be careful with floating-point comparisons since subtraction can cause precision issues. Using explicit comparisons with if statements ensures correct behavior for all numeric types including floats and doubles.

// Comparator to sort by name (alphabetical)
int compare_by_name(const void *a, const void *b) {
    Employee *emp1 = (Employee *)a;
    Employee *emp2 = (Employee *)b;
    return strcmp(emp1->name, emp2->name);
}

// Comparator to sort by salary (descending)
int compare_by_salary_desc(const void *a, const void *b) {
    Employee *emp1 = (Employee *)a;
    Employee *emp2 = (Employee *)b;
    if (emp1->salary < emp2->salary) return 1;
    if (emp1->salary > emp2->salary) return -1;
    return 0;
}

Here is the complete example that demonstrates sorting structures by different fields using different comparators. The program defines a Person structure with name and age fields, creates two comparators, and sorts an array of people by age. You could easily add another call to sort by name instead, demonstrating how the same data can be organized in multiple ways using different comparison functions.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char name[50];
    int age;
} Person;

int compare_by_age(const void *a, const void *b) {
    return ((Person *)a)->age - ((Person *)b)->age;
}

int compare_by_name(const void *a, const void *b) {
    return strcmp(((Person *)a)->name, ((Person *)b)->name);
}

int main() {
    Person people[] = {{"Charlie", 35}, {"Alice", 28}, {"Bob", 42}};
    
    qsort(people, 3, sizeof(Person), compare_by_age);
    printf("By age: ");
    for (int i = 0; i < 3; i++) printf("%s(%d) ", people[i].name, people[i].age);
    // Output: Alice(28) Charlie(35) Bob(42)
    
    return 0;
}
Tip: You can sort the same array multiple times with different comparators. This is useful for multi-column sorting or letting users choose the sort order.

Event Handler Pattern

Callbacks are essential for event-driven systems. You register a handler function that gets called when a specific event occurs. This decouples event detection from event handling, making your code more modular and flexible.

First, define a typedef for your event handler signature. This makes the code cleaner and establishes the contract that all handlers must follow. The typedef creates an alias for the function pointer type, so instead of writing the complex function pointer syntax everywhere, you can use a simple name like EventHandler. This improves readability and makes it easier to change the signature later if needed.

// Define the event handler type
// Takes an event code, returns nothing
typedef void (*EventHandler)(int event_code);

Create a global variable to store the currently registered handler. Initialize it to NULL to indicate no handler is registered yet. This variable acts as a slot that can hold any function matching the EventHandler signature. By checking for NULL before calling, you can safely handle the case where no handler has been registered, preventing crashes from calling through a null pointer.

// Storage for the registered handler
EventHandler on_button_press = NULL;

Create a registration function that allows users to set their own handler. This function simply stores the function pointer for later use. By providing a dedicated registration function instead of exposing the global variable directly, you create a clean API and can add validation or logging in the future. This pattern is common in libraries and frameworks that need to support user-defined callbacks.

// Function to register a new handler
void register_handler(EventHandler handler) {
    on_button_press = handler;
}

Create the event trigger function. This is called when an event occurs. It checks if a handler is registered and calls it if so. This function represents the core of the event system, where the actual event is detected or simulated. The NULL check is critical for safety because attempting to call a function through a NULL pointer leads to undefined behavior and typically crashes the program.

// Simulate an event occurring
void simulate_button_press(int code) {
    printf("Button pressed with code %d\n", code);
    
    // Check if a handler is registered before calling
    if (on_button_press != NULL) {
        on_button_press(code);  // Invoke the registered handler
    }
}
Always check for NULL: Before calling a function pointer, verify it is not NULL. Calling a NULL function pointer causes undefined behavior and typically crashes your program.

Now define some handler functions that match the expected signature. Each handler performs a different action when the event occurs. These functions represent different responses to the same event type. In a real application, these might send network requests, update the UI, write to log files, or perform any other action. The key is that they all share the same function signature so they can be used interchangeably.

// Handler that plays a sound
void play_sound(int code) {
    printf("  -> Playing sound #%d\n", code);
}

// Handler that logs the event
void log_event(int code) {
    printf("  -> Logged: Button event %d\n", code);
}

// Handler that sends a notification
void send_notification(int code) {
    printf("  -> Notification sent for event %d\n", code);
}

Use the system by registering a handler, then triggering events. You can change the handler at runtime to alter the behavior. This dynamic swapping is a key advantage of the callback pattern. Your event system does not need to know about all possible handlers at compile time. New handlers can be added without modifying the core event detection code, making the system highly extensible.

// Register play_sound as the handler
register_handler(play_sound);
simulate_button_press(1);
// Output: Button pressed with code 1
//         -> Playing sound #1

// Change to a different handler at runtime
register_handler(log_event);
simulate_button_press(2);
// Output: Button pressed with code 2
//         -> Logged: Button event 2

Here is the complete working example that demonstrates the event handler pattern with dynamic handler switching. The program registers one handler, triggers an event, then switches to a different handler and triggers another event. Each event produces different output based on which handler is currently registered. This pattern is the foundation of GUI programming, game engines, and embedded systems.

#include <stdio.h>

typedef void (*EventHandler)(int event_code);

EventHandler on_button_press = NULL;

void register_handler(EventHandler handler) {
    on_button_press = handler;
}

void simulate_button_press(int code) {
    printf("Button pressed: %d\n", code);
    if (on_button_press != NULL) on_button_press(code);
}

void play_sound(int code) { printf("  -> Sound #%d\n", code); }
void log_event(int code)  { printf("  -> Logged: %d\n", code); }

int main() {
    register_handler(play_sound);
    simulate_button_press(1);
    
    register_handler(log_event);
    simulate_button_press(2);
    
    return 0;
}
Real-world usage: This pattern is used in GUI frameworks, game engines, and embedded systems. Libraries like GTK, SDL, and Arduino all use callback-based event handling.

Practice Questions: Callback Functions

Task: Write a comparator function for qsort that sorts an array of strings in reverse alphabetical order (Z to A).

Show Solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int compare_desc(const void *a, const void *b) {
    // Reverse the order by swapping arguments
    return strcmp(*(const char **)b, *(const char **)a);
}

int main() {
    const char *words[] = {"apple", "cherry", "banana"};
    qsort(words, 3, sizeof(char *), compare_desc);
    
    for (int i = 0; i < 3; i++) {
        printf("%s\n", words[i]);
    }
    // cherry, banana, apple
    
    return 0;
}

Task: Write a filter function that takes an array, its size, a result array, and a predicate callback. The predicate returns 1 if the element should be kept. Return the count of filtered elements.

Show Solution
#include <stdio.h>

int filter(int *src, int size, int *dest, int (*predicate)(int)) {
    int count = 0;
    for (int i = 0; i < size; i++) {
        if (predicate(src[i])) {
            dest[count++] = src[i];
        }
    }
    return count;
}

int is_even(int x) { return x % 2 == 0; }
int is_positive(int x) { return x > 0; }

int main() {
    int nums[] = {-3, 2, 7, -1, 4, 6, -2};
    int filtered[7];
    
    int count = filter(nums, 7, filtered, is_even);
    printf("Even numbers: ");
    for (int i = 0; i < count; i++) {
        printf("%d ", filtered[i]);  // 2 4 6 -2
    }
    printf("\n");
    
    return 0;
}

Task: Create a map function that applies a transformation callback to each element of a source array and stores results in a destination array.

Show Solution
#include <stdio.h>

void map(int *src, int *dest, int size, int (*transform)(int)) {
    for (int i = 0; i < size; i++) {
        dest[i] = transform(src[i]);
    }
}

int square(int x) { return x * x; }
int negate(int x) { return -x; }
int increment(int x) { return x + 1; }

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    int result[5];
    
    map(nums, result, 5, square);
    printf("Squared: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", result[i]);  // 1 4 9 16 25
    }
    printf("\n");
    
    map(nums, result, 5, negate);
    printf("Negated: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", result[i]);  // -1 -2 -3 -4 -5
    }
    printf("\n");
    
    return 0;
}
03

Arrays of Function Pointers

You can create arrays that hold function pointers, enabling you to select and call functions by index. This technique is powerful for implementing state machines, command dispatchers, menu systems, and jump tables.

Creating Function Pointer Arrays

The syntax for declaring an array of function pointers combines array notation with function pointer syntax. Each element of the array is a pointer to a function with the specified signature. This allows you to store multiple functions in a single collection and access them by index, enabling powerful patterns like dispatch tables and dynamic function selection.

First, define the functions that will be stored in the array. All functions must have the same signature, meaning they take the same parameter types and return the same type. This uniformity is required because the array can only hold one type of function pointer.

// All functions have same signature: int(int, int)
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; }

Declare the array using the function pointer syntax with array brackets. The number in brackets specifies the array size. Initialize it with the function names, which automatically convert to function pointers. The syntax can look complex, but read it as "operations is an array of 4 pointers to functions taking two ints and returning int."

// Syntax: return_type (*array_name[size])(params)
int (*operations[4])(int, int) = {add, sub, mul, divide};

Call functions through the array using index notation followed by the arguments. The index selects which function to call, and the parentheses with arguments invoke it. This allows you to select functions dynamically at runtime based on calculations or user input rather than hardcoding which function to call.

int x = 20, y = 5;

// Call by direct index
printf("add: %d\n", operations[0](x, y));  // 25
printf("sub: %d\n", operations[1](x, y));  // 15
printf("mul: %d\n", operations[2](x, y));  // 100
printf("div: %d\n", operations[3](x, y));  // 4

Loop through the array to call each function in sequence. This is particularly useful for applying multiple operations to the same data, running test suites, or implementing pipelines where each function processes data in turn. The loop index becomes the function selector.

// Call all operations in a loop
for (int i = 0; i < 4; i++) {
    printf("Operation %d: %d\n", i, operations[i](x, y));
}
// Operation 0: 25
// Operation 1: 15
// Operation 2: 100
// Operation 3: 4

Building a Calculator with Jump Tables

A jump table (or dispatch table) uses an array of function pointers to replace long switch statements. This is more efficient and easier to extend. Instead of writing a case for each operation, you simply index into the array. Adding new operations only requires adding a new function and array entry, without modifying existing logic.

Define all the calculator operations as separate functions. Each function handles one operation and includes any necessary validation, like checking for division by zero. Keeping operations in separate functions makes the code modular and easy to test individually.

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; }
int mod(int a, int b) { return b != 0 ? a % b : 0; }

Create the jump table as an array of function pointers. Also create a parallel array of operation names for display purposes. The index in both arrays corresponds to the same operation, so index 0 is addition in both the function array and the names array.

// Jump table: index maps to operation
int (*calc[])(int, int) = {add, sub, mul, divide, mod};

// Parallel array for display names
const char *names[] = {"+", "-", "*", "/", "%"};

Use the jump table by iterating through the array or selecting by index. Each iteration calls a different operation on the same operands, producing a complete set of results. This pattern replaces verbose switch statements with clean, data-driven code.

int a = 17, b = 5;

printf("Calculator Demo: %d and %d\n", a, b);
for (int i = 0; i < 5; i++) {
    printf("%d %s %d = %d\n", a, names[i], b, calc[i](a, b));
}
// 17 + 5 = 22
// 17 - 5 = 12
// 17 * 5 = 85
// 17 / 5 = 3
// 17 % 5 = 2

Implementing a Menu System

Function pointer arrays are ideal for menu-driven programs. Each menu option corresponds to a function, and user input selects which function to call. This approach eliminates large switch statements and makes it trivial to add new menu options. The menu becomes data-driven rather than logic-driven, improving maintainability.

Define a function for each menu action. These functions contain the actual behavior for each option. In a real application, these might open files, save data, or perform complex operations. Here we use simple print statements to demonstrate the pattern.

void option_new(void)  { printf("Creating new file...\n"); }
void option_open(void) { printf("Opening file...\n"); }
void option_save(void) { printf("Saving file...\n"); }
void option_quit(void) { printf("Goodbye!\n"); }

Create parallel arrays for the menu actions and their display labels. The action array holds the function pointers, while the labels array holds the text shown to users. Keeping these separate allows easy internationalization or label changes without touching the action code.

// Array of menu action functions
void (*menu_actions[])(void) = {
    option_new,
    option_open,
    option_save,
    option_quit
};

// Array of menu labels for display
const char *menu_labels[] = {
    "New File",
    "Open File", 
    "Save File",
    "Quit"
};

Display the menu by looping through the labels array. This automatically adjusts if you add more options. The loop uses the same indexing as the actions array, keeping the display synchronized with the available functions.

printf("\n=== Menu ===\n");
for (int i = 0; i < 4; i++) {
    printf("%d. %s\n", i + 1, menu_labels[i]);
}
printf("Enter choice (1-4): ");

Handle user input by validating the choice and using it as an index into the actions array. Subtract 1 from the choice since users see 1-4 but array indices are 0-3. The function call through the array is clean and requires no switch statement.

int choice;
scanf("%d", &choice);

if (choice >= 1 && choice <= 4) {
    menu_actions[choice - 1]();  // Call selected function
} else {
    printf("Invalid choice!\n");
}

Wrap the menu in a loop to repeatedly show options until the user quits. Check for the quit option to break the loop. This creates an interactive menu system with minimal code that is easy to extend with additional options.

int choice;
do {
    // Display menu and get choice
    printf("\n=== Menu ===\n");
    for (int i = 0; i < 4; i++) {
        printf("%d. %s\n", i + 1, menu_labels[i]);
    }
    scanf("%d", &choice);
    
    if (choice >= 1 && choice <= 4) {
        menu_actions[choice - 1]();
    }
} while (choice != 4);  // 4 is Quit

State Machine Pattern

State machines can be elegantly implemented using function pointer arrays. Each state is represented by a function, and transitions update the current state index. This approach scales well because adding states only requires adding functions and updating the array. The transition logic remains unchanged regardless of how many states you have.

Define an enumeration for the states. Using an enum gives meaningful names to state indices and makes the code self-documenting. Include a COUNT value at the end to automatically track the number of states, which is useful for array sizing.

// Define states with an enum
typedef enum { 
    STATE_IDLE, 
    STATE_RUNNING, 
    STATE_PAUSED, 
    STATE_COUNT  // Automatically equals 3 (number of states)
} State;

Create a handler function for each state. These functions define what happens when the system is in each state. In a real application, these might check sensors, update displays, or perform computations. The handler is called once for each state visit.

// State handler functions
void state_idle(void) { 
    printf("System is idle. Waiting...\n"); 
}

void state_running(void) { 
    printf("System running. Processing...\n"); 
}

void state_paused(void) { 
    printf("System paused. Resuming soon...\n"); 
}

Create the state handler array using the STATE_COUNT constant for size. This ensures the array always has the right number of elements. The index positions match the enum values, so STATE_IDLE (0) maps to state_idle, STATE_RUNNING (1) maps to state_running, and so on.

// Array of state handlers indexed by State enum
void (*state_handlers[STATE_COUNT])(void) = {
    state_idle,     // Index 0 = STATE_IDLE
    state_running,  // Index 1 = STATE_RUNNING
    state_paused    // Index 2 = STATE_PAUSED
};

Track the current state with a variable and call the appropriate handler by indexing into the array. To transition, simply update the current state variable. The next handler call will automatically use the new state's function.

State current = STATE_IDLE;

// Call current state's handler
state_handlers[current]();  // "System is idle..."

// Transition to running
current = STATE_RUNNING;
state_handlers[current]();  // "System running..."

Simulate a sequence of state transitions by storing states in an array and iterating through them. This pattern is useful for testing state machines or replaying recorded sequences. Each iteration updates the current state and invokes the corresponding handler.

// Simulate state transitions
State transitions[] = {
    STATE_IDLE, STATE_RUNNING, STATE_RUNNING, 
    STATE_PAUSED, STATE_RUNNING, STATE_IDLE
};

for (int i = 0; i < 6; i++) {
    current = transitions[i];
    printf("Step %d: ", i + 1);
    state_handlers[current]();
}

Practice Questions: Function Pointer Arrays

Task: Create three functions that print greetings in different languages (English, French, German). Store them in an array and call each one.

Show Solution
#include <stdio.h>

void greet_en(void) { printf("Hello!\n"); }
void greet_fr(void) { printf("Bonjour!\n"); }
void greet_de(void) { printf("Guten Tag!\n"); }

int main() {
    void (*greetings[3])(void) = {greet_en, greet_fr, greet_de};
    const char *languages[] = {"English", "French", "German"};
    
    for (int i = 0; i < 3; i++) {
        printf("%s: ", languages[i]);
        greetings[i]();
    }
    
    return 0;
}

Task: Create a command dispatcher that maps single character commands ('a' for add, 's' for subtract, etc.) to functions using an array indexed by character code.

Show Solution
#include <stdio.h>

void cmd_add(void) { printf("Adding...\n"); }
void cmd_sub(void) { printf("Subtracting...\n"); }
void cmd_mul(void) { printf("Multiplying...\n"); }
void cmd_unknown(void) { printf("Unknown command\n"); }

int main() {
    // Create dispatch table (128 entries for ASCII)
    void (*commands[128])(void);
    
    // Initialize all to unknown
    for (int i = 0; i < 128; i++) {
        commands[i] = cmd_unknown;
    }
    
    // Map specific commands
    commands['a'] = cmd_add;
    commands['s'] = cmd_sub;
    commands['m'] = cmd_mul;
    
    // Test
    char inputs[] = {'a', 's', 'm', 'x'};
    for (int i = 0; i < 4; i++) {
        printf("Command '%c': ", inputs[i]);
        commands[(int)inputs[i]]();
    }
    
    return 0;
}

Task: Simulate object-oriented polymorphism. Create a "Shape" structure with a function pointer array for area and perimeter. Create "Circle" and "Rectangle" that use different calculation functions.

Show Solution
#include <stdio.h>
#define PI 3.14159

typedef struct {
    double (*area)(double, double);
    double (*perimeter)(double, double);
} ShapeVTable;

double circle_area(double r, double unused) { 
    return PI * r * r; 
}
double circle_perim(double r, double unused) { 
    return 2 * PI * r; 
}
double rect_area(double w, double h) { 
    return w * h; 
}
double rect_perim(double w, double h) { 
    return 2 * (w + h); 
}

ShapeVTable circle_vtable = {circle_area, circle_perim};
ShapeVTable rect_vtable = {rect_area, rect_perim};

typedef struct {
    ShapeVTable *vtable;
    double dim1, dim2;
} Shape;

int main() {
    Shape circle = {&circle_vtable, 5.0, 0.0};
    Shape rect = {&rect_vtable, 4.0, 6.0};
    
    printf("Circle - Area: %.2f, Perimeter: %.2f\n",
           circle.vtable->area(circle.dim1, circle.dim2),
           circle.vtable->perimeter(circle.dim1, circle.dim2));
    
    printf("Rectangle - Area: %.2f, Perimeter: %.2f\n",
           rect.vtable->area(rect.dim1, rect.dim2),
           rect.vtable->perimeter(rect.dim1, rect.dim2));
    
    return 0;
}
04

Typedef for Clarity

Function pointer syntax can be confusing. Using typedef creates meaningful type names that make your code more readable and self-documenting. This is especially valuable when function pointers are used throughout a codebase.

Creating Type Aliases

A typedef for a function pointer creates a new type name that you can use like any other type. This dramatically simplifies declarations and makes the intent clear. Instead of repeating the complex function pointer syntax everywhere, you define it once and use a simple name. This is the standard practice in professional C codebases.

Compare the confusing raw syntax with the clean typedef approach. Without typedef, declaring a function pointer requires remembering the exact placement of parentheses and asterisks. With typedef, you create a named type that reads naturally and prevents syntax errors.

// Without typedef - confusing syntax
int (*operation)(int, int);  // Hard to read and remember

// With typedef - clear and self-documenting!
typedef int (*MathOperation)(int, int);

The typedef syntax mirrors the function pointer declaration but adds the typedef keyword at the beginning and a type name where the variable name would go. Once defined, use the new type name just like int, float, or any built-in type. The compiler treats it as a complete type.

// Create the typedef
typedef int (*MathOperation)(int, int);

// Now use it like any type
MathOperation op1;         // Declare a variable
MathOperation op2 = NULL;  // Initialize to NULL

Define functions that match the typedef signature. These functions can be assigned to variables of the typedef type. The compiler verifies that only functions with matching signatures are assigned, catching errors at compile time rather than runtime.

// Functions matching MathOperation signature
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int sub(int a, int b) { return a - b; }

Assign functions to typedef variables and call them. The syntax is identical to calling a regular function. This makes code using function pointers much more readable, as readers do not need to parse complex pointer syntax to understand what is happening.

// Assign functions to typedef variables
MathOperation op1 = add;
MathOperation op2 = mul;

// Call through the typedef variables
printf("Sum: %d\n", op1(5, 3));      // 8
printf("Product: %d\n", op2(5, 3));  // 15

Array declarations become dramatically cleaner with typedef. Without it, array syntax combined with function pointer syntax creates a nearly unreadable mess. With typedef, the array declaration looks like any normal array of a simple type.

// Without typedef - very confusing
int (*operations_raw[4])(int, int) = {add, sub, mul, sub};

// With typedef - clean and readable
MathOperation operations[4] = {add, sub, mul, sub};

// Use the array normally
for (int i = 0; i < 4; i++) {
    printf("Result: %d\n", operations[i](10, 3));
}

Comparison: With and Without Typedef

Without Typedef With Typedef
int (*op)(int, int); MathOp op;
int (*ops[4])(int, int); MathOp ops[4];
void func(int (*cb)(int)); void func(Callback cb);
int (*get_op())(int, int); MathOp get_op();

Common Typedef Patterns

Here are some common patterns you will encounter when working with function pointer typedefs in real-world C code. Each pattern serves a specific purpose and following these conventions makes your code immediately understandable to other C programmers. Learning these patterns helps you recognize them in libraries and frameworks.

Comparators are used for sorting and searching. They take two generic pointers and return an integer indicating order. This is the same signature used by the standard library qsort() and bsearch() functions.

// Comparator - used for sorting and searching
typedef int (*Comparator)(const void *, const void *);

// Example usage with qsort
Comparator cmp = my_compare_function;
qsort(array, count, sizeof(int), cmp);

Event callbacks handle notifications from a system. They typically take an event code or event structure and return nothing. This pattern is used extensively in GUI frameworks, game engines, and embedded systems.

// EventCallback - handles events, returns nothing
typedef void (*EventCallback)(int event_code);

// Example: registering a button click handler
EventCallback on_click = handle_button_click;
register_button_handler(button_id, on_click);

Predicates test conditions and return true (nonzero) or false (zero). They are used for filtering, searching, and conditional logic. The name comes from logic where a predicate is a statement that is either true or false.

// Predicate - tests a condition, returns 1 or 0
typedef int (*Predicate)(int value);

// Example predicates
int is_even(int x) { return x % 2 == 0; }
int is_positive(int x) { return x > 0; }

Transformers convert input values to output values of the same type. They are used in map operations, data processing pipelines, and mathematical transformations. Chaining multiple transformers creates powerful data processing flows.

// Transformer - maps input to output
typedef int (*Transformer)(int input);

// Example transformers
int square(int x) { return x * x; }
int double_it(int x) { return x * 2; }
int negate(int x) { return -x; }

Handlers with context accept additional data along with the event. The void pointer allows passing any type of context data, which is cast back to the expected type inside the handler. This pattern enables stateful callbacks.

// Handler with context - allows passing extra data
typedef void (*Handler)(void *context, int data);

// Example: handler that uses context
void log_handler(void *ctx, int data) {
    FILE *log = (FILE *)ctx;  // Cast context to file pointer
    fprintf(log, "Data: %d\n", data);
}

Factory functions create and return new objects. They take no parameters and return a void pointer to the created object. This pattern is used for plugin systems and abstract object creation where the exact type is determined at runtime.

// Factory - creates objects dynamically
typedef void *(*Factory)(void);

// Example factory
void *create_player(void) {
    Player *p = malloc(sizeof(Player));
    // Initialize player...
    return p;
}

Using Typedef in Function Signatures

When functions accept or return function pointers, typedef makes the signatures dramatically cleaner and easier to understand. Without typedef, these signatures become nearly unreadable puzzles of asterisks and parentheses. With typedef, the intent is immediately clear to anyone reading the code.

First, define your typedef and the functions that match it. This establishes the contract for what functions can be used with your higher-order functions. Having this typedef in a header file allows consistent usage across multiple source files.

// Define the typedef
typedef int (*BinaryOp)(int, int);

// Functions matching the typedef
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

Create a function that accepts a function pointer as a parameter. With typedef, the parameter declaration is simple and clear. Compare this to the raw syntax int apply(int (*operation)(int, int), int x, int y) which is much harder to read at a glance.

// Function that ACCEPTS a function pointer
// Clean: BinaryOp instead of int (*)(int, int)
int apply(BinaryOp operation, int x, int y) {
    return operation(x, y);
}

// Usage
int result = apply(add, 10, 5);  // Returns 15

Create a function that returns a function pointer. Without typedef, the syntax for returning a function pointer is int (*get_operation(char))(int, int), which is almost impossible to parse. With typedef, it reads like any normal function that returns a value.

// Function that RETURNS a function pointer
// Clean: returns BinaryOp instead of cryptic syntax
BinaryOp get_operation(char symbol) {
    switch (symbol) {
        case '+': return add;
        case '-': return sub;
        case '*': return mul;
        default:  return NULL;
    }
}

Use the function that returns a function pointer by storing its result and checking for NULL before calling. This pattern is used for dynamic dispatch where the operation is determined by user input or configuration at runtime.

// Get operation based on user input
char user_choice = '-';
BinaryOp op = get_operation(user_choice);

// Always check for NULL before calling
if (op != NULL) {
    printf("10 - 5 = %d\n", op(10, 5));  // 5
} else {
    printf("Unknown operation\n");
}

Combine both patterns for powerful higher-order programming. Functions can accept function pointers, return them, or both. This enables factory patterns, strategy patterns, and functional programming styles in C.

// Using both patterns together
int main() {
    // Pass function directly
    printf("Result: %d\n", apply(add, 10, 5));  // 15
    
    // Get function dynamically, then use it
    BinaryOp op = get_operation('*');
    printf("Result: %d\n", apply(op, 10, 5));  // 50
    
    return 0;
}
Best practice: Always use typedef for function pointers in production code. Name them descriptively (e.g., Comparator, EventHandler, ProgressCallback) to convey their purpose.

Practice Questions: Typedef

Task: Create a typedef StringValidator for functions that take a const char* and return an int (1 for valid, 0 for invalid). Write validators for non-empty and length check.

Show Solution
#include <stdio.h>
#include <string.h>

typedef int (*StringValidator)(const char *);

int is_not_empty(const char *str) {
    return str != NULL && strlen(str) > 0;
}

int is_short(const char *str) {
    return str != NULL && strlen(str) <= 10;
}

int main() {
    StringValidator validators[] = {is_not_empty, is_short};
    
    const char *test = "Hello";
    printf("'%s' not empty: %d\n", test, validators[0](test));  // 1
    printf("'%s' is short: %d\n", test, validators[1](test));   // 1
    
    return 0;
}

Task: Create a Pipeline type for functions that transform an integer. Build a run_pipeline function that applies an array of transformations in sequence.

Show Solution
#include <stdio.h>

typedef int (*Pipeline)(int);

int double_it(int x) { return x * 2; }
int add_ten(int x) { return x + 10; }
int square(int x) { return x * x; }

int run_pipeline(int value, Pipeline *stages, int count) {
    for (int i = 0; i < count; i++) {
        value = stages[i](value);
    }
    return value;
}

int main() {
    Pipeline stages[] = {double_it, add_ten, square};
    
    int input = 5;
    int output = run_pipeline(input, stages, 3);
    
    // 5 -> 10 -> 20 -> 400
    printf("%d -> %d\n", input, output);
    
    return 0;
}
05

Practical Applications

Function pointers enable powerful design patterns in C. Let us explore real-world applications including plugin systems, strategy patterns, and generic data processing.

Generic Array Processing

By combining function pointers with void pointers, you can create truly generic functions that work with any data type, similar to how the standard library does it.

#include <stdio.h>
#include <string.h>

typedef void (*PrintFunc)(const void *);

void print_int(const void *p) {
    printf("%d", *(const int *)p);
}

void print_double(const void *p) {
    printf("%.2f", *(const double *)p);
}

void print_string(const void *p) {
    printf("%s", *(const char **)p);
}

void print_array(void *arr, int count, int elem_size, PrintFunc print) {
    printf("[");
    for (int i = 0; i < count; i++) {
        if (i > 0) printf(", ");
        print((char *)arr + i * elem_size);
    }
    printf("]\n");
}

int main() {
    int nums[] = {10, 20, 30, 40};
    double prices[] = {9.99, 19.99, 29.99};
    const char *names[] = {"Alice", "Bob", "Charlie"};
    
    print_array(nums, 4, sizeof(int), print_int);
    // [10, 20, 30, 40]
    
    print_array(prices, 3, sizeof(double), print_double);
    // [9.99, 19.99, 29.99]
    
    print_array(names, 3, sizeof(char *), print_string);
    // [Alice, Bob, Charlie]
    
    return 0;
}

Plugin Architecture

Function pointers enable extensible systems where new functionality can be added without modifying existing code. This is the foundation of plugin architectures.

#include <stdio.h>

#define MAX_PLUGINS 10

typedef struct {
    const char *name;
    void (*init)(void);
    void (*process)(const char *data);
    void (*cleanup)(void);
} Plugin;

Plugin plugins[MAX_PLUGINS];
int plugin_count = 0;

void register_plugin(Plugin p) {
    if (plugin_count < MAX_PLUGINS) {
        plugins[plugin_count++] = p;
        printf("Registered plugin: %s\n", p.name);
    }
}

void init_all_plugins(void) {
    for (int i = 0; i < plugin_count; i++) {
        if (plugins[i].init) plugins[i].init();
    }
}

void process_with_all(const char *data) {
    for (int i = 0; i < plugin_count; i++) {
        if (plugins[i].process) plugins[i].process(data);
    }
}

// Example plugins
void logger_init(void) { printf("Logger ready\n"); }
void logger_process(const char *d) { printf("[LOG] %s\n", d); }

void stats_init(void) { printf("Stats ready\n"); }
void stats_process(const char *d) { printf("[STATS] len=%zu\n", strlen(d)); }

int main() {
    Plugin logger = {"Logger", logger_init, logger_process, NULL};
    Plugin stats = {"Stats", stats_init, stats_process, NULL};
    
    register_plugin(logger);
    register_plugin(stats);
    
    init_all_plugins();
    process_with_all("Hello World");
    
    return 0;
}

Strategy Pattern

The strategy pattern allows you to swap algorithms at runtime. This is useful for implementing different behaviors based on configuration or user choice.

#include <stdio.h>

typedef double (*TaxStrategy)(double amount);

double tax_standard(double amount) {
    return amount * 0.20;  // 20% tax
}

double tax_reduced(double amount) {
    return amount * 0.05;  // 5% reduced rate
}

double tax_exempt(double amount) {
    return 0.0;  // No tax
}

typedef struct {
    const char *product;
    double price;
    TaxStrategy calculate_tax;
} Item;

double calculate_total(Item *items, int count) {
    double total = 0;
    for (int i = 0; i < count; i++) {
        double tax = items[i].calculate_tax(items[i].price);
        printf("%s: $%.2f + $%.2f tax\n", 
               items[i].product, items[i].price, tax);
        total += items[i].price + tax;
    }
    return total;
}

int main() {
    Item cart[] = {
        {"Electronics", 100.00, tax_standard},
        {"Food", 50.00, tax_reduced},
        {"Medicine", 30.00, tax_exempt}
    };
    
    double total = calculate_total(cart, 3);
    printf("Total: $%.2f\n", total);
    // Electronics: $100.00 + $20.00 tax
    // Food: $50.00 + $2.50 tax
    // Medicine: $30.00 + $0.00 tax
    // Total: $202.50
    
    return 0;
}

Timer and Scheduler

Function pointers are essential for scheduling systems where different tasks need to be executed at different times.

#include <stdio.h>

typedef void (*TaskFunc)(void);

typedef struct {
    const char *name;
    TaskFunc execute;
    int interval;
    int next_run;
} Task;

void task_backup(void) { printf("  Backing up data...\n"); }
void task_cleanup(void) { printf("  Cleaning temp files...\n"); }
void task_report(void) { printf("  Generating report...\n"); }

void run_scheduler(Task *tasks, int count, int ticks) {
    for (int t = 0; t < ticks; t++) {
        printf("Tick %d:\n", t);
        for (int i = 0; i < count; i++) {
            if (t >= tasks[i].next_run) {
                printf("  Running: %s\n", tasks[i].name);
                tasks[i].execute();
                tasks[i].next_run = t + tasks[i].interval;
            }
        }
    }
}

int main() {
    Task tasks[] = {
        {"Backup", task_backup, 3, 0},
        {"Cleanup", task_cleanup, 2, 0},
        {"Report", task_report, 5, 0}
    };
    
    run_scheduler(tasks, 3, 6);
    
    return 0;
}

Practice Questions: Applications

Task: Create a reduce function that combines all elements of an array into a single value using a binary function (like sum or product).

Show Solution
#include <stdio.h>

typedef int (*Reducer)(int, int);

int reduce(int *arr, int size, int initial, Reducer fn) {
    int result = initial;
    for (int i = 0; i < size; i++) {
        result = fn(result, arr[i]);
    }
    return result;
}

int sum(int a, int b) { return a + b; }
int product(int a, int b) { return a * b; }
int max(int a, int b) { return a > b ? a : b; }

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    
    printf("Sum: %d\n", reduce(nums, 5, 0, sum));       // 15
    printf("Product: %d\n", reduce(nums, 5, 1, product)); // 120
    printf("Max: %d\n", reduce(nums, 5, nums[0], max));  // 5
    
    return 0;
}

Task: Create a system that maps command-line flags to handler functions. Support -h for help, -v for version, and -n followed by a name argument.

Show Solution
#include <stdio.h>
#include <string.h>

typedef void (*ArgHandler)(const char *value);

typedef struct {
    const char *flag;
    ArgHandler handler;
    int has_value;
} ArgOption;

void handle_help(const char *v) {
    printf("Usage: program [-h] [-v] [-n name]\n");
}

void handle_version(const char *v) {
    printf("Version 1.0.0\n");
}

void handle_name(const char *v) {
    printf("Hello, %s!\n", v);
}

int main(int argc, char *argv[]) {
    ArgOption options[] = {
        {"-h", handle_help, 0},
        {"-v", handle_version, 0},
        {"-n", handle_name, 1}
    };
    int opt_count = 3;
    
    for (int i = 1; i < argc; i++) {
        for (int j = 0; j < opt_count; j++) {
            if (strcmp(argv[i], options[j].flag) == 0) {
                const char *val = NULL;
                if (options[j].has_value && i + 1 < argc) {
                    val = argv[++i];
                }
                options[j].handler(val);
                break;
            }
        }
    }
    
    return 0;
}

Key Takeaways

Function Pointer Syntax

Declare with return_type (*name)(params) - parentheses around *name are essential

Callbacks Enable Flexibility

Pass functions as arguments to customize behavior without modifying existing code

Arrays for Dispatch Tables

Store function pointers in arrays to select operations by index - great for menus and state machines

Typedef for Readability

Use typedef to create meaningful names for function pointer types

qsort Pattern

Standard library uses callbacks - qsort takes a comparator function for custom sorting

Real-World Patterns

Enables plugins, strategy pattern, event handlers, and polymorphism in C

Knowledge Check

Test your understanding of function pointers in C:

1 What does the declaration int (*fp)(int, int); declare?
2 If void (*handler)(int) points to a function, which calls are valid?
3 What is the purpose of a callback function?
4 What does typedef int (*Comparator)(const void *, const void *); create?
5 In qsort's comparator, what should the function return if the first element is greater?
6 What is a jump table (dispatch table)?
Answer all questions to check your score