Function pointers are one of the most powerful—and often misunderstood—features in the C language. They enable flexible control flow, decoupled design, and runtime behavior selection, making them essential for systems programming, embedded software, and performance-critical applications.
This article walks through six advanced and practical application scenarios of function pointers, each illustrated with clear examples and real-world use cases.
🔔 Callback Functions #
A callback function is a function passed as an argument to another function and invoked when a specific event or condition occurs. This pattern is widely used in event-driven systems, libraries, and drivers.
Typical use cases: event handling, interrupt hooks, library extensibility.
void handle_event(int event_type, void (*callback)(void))
{
printf("event %d occurred\n", event_type);
if (callback)
{
callback(); // Invoke callback if provided
}
}
void callback_function()
{
printf("callback function called\n");
}
int main()
{
handle_event(1, callback_function);
handle_event(2, NULL);
return 0;
}
**Output:**
event 1 occurred callback function called event 2 occurred
## ⚙️ Function Parameterization
Function pointers allow functions to accept *behavior* as a parameter, not just data. This technique is often used to implement generic algorithms.
**Scenario:** Applying a custom operation to each element of an array.
```c
void process_array(int *array, size_t size, int (*process)(int))
{
for (size_t i = 0; i < size; i++)
{
array[i] = process(array[i]);
}
}
int increment(int n)
{
return n + 1;
}
int main()
{
int array[] = {1, 2, 3, 4, 5};
process_array(array, 5, increment);
return 0;
}
Result: 2 3 4 5 6
🔃 Sorting with Custom Comparison Functions #
Many standard library algorithms rely on function pointers to define comparison logic. This allows a single algorithm to support multiple ordering strategies.
Scenario: Using qsort() with custom comparison functions.
typedef int (*compare_func_t)(const void *, const void *);
int compare_int(const void *a, const void *b)
{
return (*(int *)a - *(int *)b); // Ascending
}
int compare_reverse_int(const void *a, const void *b)
{
return (*(int *)b - *(int *)a); // Descending
}
int main()
{
int array[] = {3, 1, 4, 1, 5, 9};
qsort(array, 6, sizeof(int), compare_int);
qsort(array, 6, sizeof(int), compare_reverse_int);
return 0;
}
This pattern is fundamental in standard C libraries and many embedded frameworks.
🧮 Array of Function Pointers (Dispatch Tables) #
An array of function pointers creates a dispatch table, enabling dynamic selection of functionality based on runtime input.
Scenario: Implementing a simple operation dispatcher.
void add(int a, int b) { printf("%d\n", a + b); }
void subtract(int a, int b) { printf("%d\n", a - b); }
void multiply(int a, int b) { printf("%d\n", a * b); }
void divide(int a, int b) { printf("%d\n", a / b); }
typedef void (*operation_func_t)(int, int);
int main()
{
operation_func_t ops[] = {add, subtract, multiply, divide};
for (int i = 0; i < 4; i++)
{
ops[i](10, 5);
}
return 0;
}
Why it matters: This technique is common in protocol handlers, interpreters, state machines, and embedded command processors.
🧠 Backtracking Algorithms with Callbacks #
Function pointers are useful in recursive algorithms where the action performed on a solution should be configurable.
Scenario: Generating permutations with a callback invoked at each solution.
typedef void (*callback_func_t)(const int *, size_t);
void print_array(const int *arr, size_t len)
{
for (size_t i = 0; i < len; i++)
printf("%d ", arr[i]);
printf("\n");
}
void permute(int *nums, size_t len, size_t depth, callback_func_t callback)
{
if (depth == len)
{
callback(nums, len);
return;
}
for (size_t i = depth; i < len; i++)
{
int tmp = nums[depth];
nums[depth] = nums[i];
nums[i] = tmp;
permute(nums, len, depth + 1, callback);
tmp = nums[depth];
nums[depth] = nums[i];
nums[i] = tmp;
}
}
This approach cleanly separates algorithm logic from output behavior.
🧱 Polymorphism in C Using Function Pointers #
Although C is not object-oriented, function pointers inside structures can simulate runtime polymorphism, similar to virtual functions in C++.
Scenario: A generic shape interface with specific implementations.
typedef struct shape
{
void (*draw)(struct shape *);
} shape_t;
typedef struct
{
shape_t shape;
int x, y, r;
} circle_t;
typedef struct
{
shape_t shape;
int x, y, w, h;
} rectangle_t;
void circle_draw(shape_t *s)
{
circle_t *c = (circle_t *)s;
printf("Circle at (%d, %d), r=%d\n", c->x, c->y, c->r);
}
void rectangle_draw(shape_t *s)
{
rectangle_t *r = (rectangle_t *)s;
printf("Rectangle at (%d, %d), %dx%d\n", r->x, r->y, r->w, r->h);
}
This pattern is widely used in GUI toolkits, drivers, and RTOS object models.
🧾 Summary #
Function pointers unlock powerful design techniques in C, enabling:
- Callbacks for event-driven programming
- Behavior injection via function parameterization
- Reusable algorithms through custom comparison logic
- Dispatch tables for dynamic command handling
- Flexible recursion in backtracking algorithms
- Runtime polymorphism without object-oriented language support
Mastering function pointers is a key milestone in becoming a proficient systems or embedded C developer.