Function Pointers, Callbacks & Void Pointers
If data pointers are C's right arm, function pointers
and void pointers are its left — together they form the
foundation of generic programming in a language without
templates, inheritance, or runtime type information. The
Linux kernel's device driver model, the
qsort standard library function, GUI event
loops, and plugin architectures all rely on these three
concepts. They are the gateway from "I can write C" to
"I can design extensible C systems."
1. void*: The Universal Pointer
A void* ("pointer to void") is C's generic
pointer type. It can hold the address of
any object, regardless of type —
int, double,
struct, anything. This is the mechanism
that makes generic data structures possible.
1.1 The Rules of void*
-
Assignment: Any pointer type
implicitly converts to
void*(no cast needed). -
Conversion back:
void*implicitly converts to any pointer type in C (C++ requires an explicit cast). -
Dereferencing: You
cannot dereference a
void*directly — the compiler doesn't know the size of the pointed-to object. -
Arithmetic: You
cannot perform pointer arithmetic
on
void*— same reason (GCC allows it as an extension, treatingvoidas size 1, but it's non-standard).
#include <stdio.h>
/* A truly generic print function — the type tag is explicit */
void generic_print(void *ptr, char type) {
switch (type) {
case 'i': printf("int: %d\n", *(int*)ptr); break;
case 'f': printf("float: %.2f\n", *(float*)ptr); break;
case 'c': printf("char: %c\n", *(char*)ptr); break;
case 's': printf("string: %s\n", (char*)ptr); break; /* char* */
default: printf("unknown type\n");
}
}
int main() {
int i = 42;
float f = 3.14159f;
char c = 'X';
char *s = "Hello";
generic_print(&i, 'i');
generic_print(&f, 'f');
generic_print(&c, 'c');
generic_print(s, 's'); /* s is already a pointer */
return 0;
}
The pattern above — a void* plus a type tag
— is called tagged union style. It's
verbose and error-prone (what if the tag doesn't
match?). A better approach for complex systems is to
embed function pointers that know how to operate on the
data (see callbacks below).
2. Function Pointers: Storing Behavior, Not Just Data
A function pointer stores the memory address of a function. When you call through it, the CPU jumps to that address and executes. The syntax is the trickiest part — let's master it step by step.
2.1 Declaration Syntax Demystified
/* The general form:
return_type (*pointer_name)(param_type1, param_type2, ...);
Read from the inside out:
1. (*ptr) — ptr is a pointer
2. (*ptr)(...) — to a function taking these parameters
3. return_type (*ptr)(...) — returning return_type */
/* Concrete examples: */
int (*math_op)(int, int); /* pointer to function(int,int)->int */
void (*callback)(void); /* pointer to function(void)->void */
double (*transform)(double); /* pointer to function(double)->double */
char* (*get_string)(int, float); /* pointer to function(int,float)->char* */
/* COMMON TRAP: missing parentheses */
int *bad_op(int, int); /* This is a FUNCTION returning int* — NOT a pointer! */
int (*good_op)(int, int); /* This IS a function pointer */
2.2 Assignment and Invocation
In C, a function's name (without parentheses) decays to a pointer to that function, just as an array name decays to a pointer:
#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 main() {
int (*op)(int, int); /* declare a function pointer */
op = add; /* assign: function name -> pointer (no & needed) */
printf("add: %d\n", op(10, 5)); /* call through pointer */
op = ⊂ /* &func also works (explicit) */
printf("sub: %d\n", (*op)(10, 5)); /* dereference-then-call also works */
/* All four calling styles are equivalent in C:
op(10, 5) — most common, looks like a regular call
(*op)(10, 5) — explicit dereference
(****op)(10, 5) — yes, even this works (deref of function pointer
yields the function, which decays back to a pointer) */
return 0;
}
2.3 Using typedef to Simplify Function
Pointer Syntax
Raw function pointer declarations become unreadable
fast, especially as parameters or return types.
typedef is the cure:
/* Without typedef — hard to read, easy to get wrong */
void sort_items(void *base, int count,
int (*compare)(const void*, const void*));
/* With typedef — clear and self-documenting */
typedef int (*CompareFunc)(const void*, const void*);
void sort_items(void *base, int count, CompareFunc compare);
/* Another example: a dispatch table */
typedef void (*CommandHandler)(int argc, char *argv[]);
CommandHandler dispatch[256]; /* array of function pointers — clean! */
/* versus:
void (*dispatch[256])(int, char**); — cryptic, parenthesized nightmare */
3. Callbacks: The Strategy Pattern in C
A callback is a function pointer passed as an argument, allowing the called function to "call back" into the caller's code. This is C's primary mechanism for customizing behavior — the foundation of the Strategy pattern.
3.1 The Canonical Example: qsort
The C standard library's qsort is the
perfect callback demonstration. It sorts
any array, but you supply the comparison logic:
#include <stdio.h>
#include <stdlib.h>
/* qsort expects a comparison function with this signature:
int compare(const void *a, const void *b)
Returns: < 0 if a < b, 0 if a == b, > 0 if a > b */
int compare_ints(const void *a, const void *b) {
int ia = *(const int*)a;
int ib = *(const int*)b;
/* Subtle: return ia - ib can overflow for large values.
This explicit comparison is safer: */
return (ia > ib) - (ia < ib);
}
int compare_strings(const void *a, const void *b) {
/* qsort passes pointers to array elements. For a char* array,
each element is a char*, so qsort passes char** to us. */
const char *sa = *(const char**)a;
const char *sb = *(const char**)b;
return strcmp(sa, sb); /* or roll your own for case-insensitive */
}
int main() {
int nums[] = {42, 7, 99, 13, 1, 55};
int n = sizeof(nums) / sizeof(nums[0]);
qsort(nums, n, sizeof(int), compare_ints);
for (int i = 0; i < n; i++) printf("%d ", nums[i]);
printf("\n"); /* 1 7 13 42 55 99 */
return 0;
}
3.2 Building a Generic Array Iterator
Let's build our own generic function that applies a callback to every element of an array of any type:
#include <stdio.h>
/* for_each: apply 'func' to every element of a generic array.
base — pointer to first element
count — number of elements
elem_size — size of each element (needed for pointer arithmetic)
func — callback: takes a void* to one element */
typedef void (*ElementFunc)(void *element);
void for_each(void *base, size_t count, size_t elem_size, ElementFunc func) {
char *ptr = (char*)base; /* cast to char* for byte-level arithmetic */
for (size_t i = 0; i < count; i++) {
func(ptr);
ptr += elem_size; /* advance by one element */
}
}
/* A concrete callback: print an int */
void print_int(void *elem) {
printf("%d ", *(int*)elem);
}
/* A concrete callback: double an int in-place */
void double_int(void *elem) {
*(int*)elem *= 2;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
printf("Original: ");
for_each(arr, 5, sizeof(int), print_int);
printf("\n");
for_each(arr, 5, sizeof(int), double_int);
printf("Doubled: ");
for_each(arr, 5, sizeof(int), print_int);
printf("\n");
return 0;
}
3.3 Function Pointer Arrays: Dispatch Tables
Replace long switch statements with an
array of function pointers indexed by an operation code.
This is how command parsers, state machines, and virtual
machines achieve O(1) dispatch:
#include <stdio.h>
/* Define operation codes */
typedef enum { OP_ADD = 0, OP_SUB, OP_MUL, OP_DIV, OP_COUNT } OpCode;
/* Operation implementations */
int op_add(int a, int b) { return a + b; }
int op_sub(int a, int b) { return a - b; }
int op_mul(int a, int b) { return a * b; }
int op_div(int a, int b) { return b != 0 ? a / b : 0; }
int main() {
/* The dispatch table: index by OpCode, get a function pointer */
typedef int (*BinOp)(int, int);
BinOp ops[OP_COUNT] = { op_add, op_sub, op_mul, op_div };
/* Usage: no if-else chain, no switch — O(1) dispatch */
OpCode code = OP_MUL;
printf("5 %s 3 = %d\n",
(const char*[]){"+","-","*","/"}[code],
ops[code](5, 3));
return 0;
}
4. Common Pitfalls
4.1 Mismatched Function Pointer Signatures
Calling a function through a pointer with the wrong signature is undefined behavior, and the compiler may not warn you:
int add(int a, int b) { return a + b; }
void (*wrong_ptr)(void) = (void(*)(void))add; /* forced cast */
wrong_ptr(); /* UNDEFINED: wrong calling convention, stack corruption */
4.2 Confusing void* and Function Pointers
void* is for
data pointers. A function pointer is
not guaranteed to fit in a
void* (some architectures have separate
address spaces for code and data). POSIX requires this
conversion to work; ISO C does not:
void *vp = (void*)add; /* NON-PORTABLE: may not work on all platforms */
/* For portable code, store function pointers in function-pointer variables
or use a union if you need to store either type. */
4.3 Forgetting the Size Parameter in Generic Functions
When writing generic functions that operate on
void* arrays, you
must know the element size. Without it,
you cannot advance the pointer correctly:
/* WRONG: treats all elements as 1 byte */
void bad_iterate(void *arr, int count, void (*func)(void*)) {
for (int i = 0; i < count; i++) {
func(arr + i); /* BUG: i is not multiplied by element size */
}
}
5. Key Takeaways
-
void*is the universal data pointer — it can hold any pointer type but cannot be dereferenced or used in arithmetic directly. Use it with an explicit cast and knowledge of the underlying type. -
Function pointer syntax is arcane;
always use
typedefto create readable aliases. The form is:typedef ReturnType (*AliasName)(ParamTypes); -
Callbacks (function pointers passed as arguments)
are C's mechanism for customizing behavior.
qsortis the canonical example — study it until the pattern is second nature. -
Dispatch tables (arrays of function
pointers) replace long
switch/if-elsechains with O(1) indexed lookup. This is how interpreters, command handlers, and state machines achieve performance and modularity. -
Never cast a function pointer to
void*for portable code. Store function pointers in properly-typed function pointer variables or use aunion.
6. Practice Exercises
Exercise 1: Generic Array Filter
Implement
int array_filter(void *base, size_t count, size_t
elem_size, int (*predicate)(const void*))
that removes elements for which
predicate returns 0, shifting remaining
elements to fill the gaps. Return the new count. The
predicate receives a pointer to one element. Test with
an int array filtering out odd numbers.
typedef int (*Predicate)(const void*);
/* Returns the new number of elements after filtering */
size_t array_filter(void *base, size_t count, size_t elem_size,
Predicate pred);
Hint: Maintain a write index separate
from the read index. Use memmove (not
memcpy — source and destination overlap) to
shift elements.
Exercise 2: Simple Plugin System
Design a small plugin system. Define a struct
Plugin containing a name string and two
function pointers: int (*init)(void) and
void (*run)(void). Create three "plugins"
(e.g., Logger, Notifier, Analyzer) with different
implementations. Store them in an array and iterate,
calling each plugin's init then
run.
typedef struct {
const char *name;
int (*init)(void);
void (*run)(void);
} Plugin;
Exercise 3: Safe void* Linked List
Rewrite the linked list from the previous lesson to
store void* data instead of
int. Add a
void (*print)(const void*) parameter to the
print function so the list doesn't need to know the data
type. Add a
Node* list_find(Node *head, const void *target, int
(*compare)(const void*, const void*))
function. Test with both int and string data.
Exercise 4: Calculator with Dispatch Table
Build a command-line RPN (Reverse Polish Notation)
calculator. Parse tokens from argv. If a
token is an operator (+, -,
*, /), use a dispatch table
mapping operator characters to function pointers to
perform the operation on the two most recent stack
values. If a token is a number, push it. Print the final
result. Handle division by zero and insufficient
operands gracefully.