All Courses C Notes
C Advanced

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, treating void as 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 typedef to create readable aliases. The form is: typedef ReturnType (*AliasName)(ParamTypes);
  • Callbacks (function pointers passed as arguments) are C's mechanism for customizing behavior. qsort is the canonical example — study it until the pattern is second nature.
  • Dispatch tables (arrays of function pointers) replace long switch/if-else chains 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 a union.

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.