How Do I Build Generic Data Structures with Pointers and Functions?
Linked lists with void pointers and function pointers — building Java-like generics by hand
After this lesson, you will be able to:
- Describe the four-callback pattern for generic containers (build, print, compare, clean)
- Implement a generic linked list node using
void *data - Write a
free_listfunction that savesnextbefore freeing each node - Write type-specific callback functions that cast
void *to the correct type
The Big Idea
You have function pointers (parameterize behavior) and void pointers (parameterize type). Combine them, and you can build a container that stores any type and operates on it through callbacks — without the container knowing the type.
This is C’s version of Java generics: ArrayList<T> built from raw pointers and function addresses.
The Generic Container Pattern
Four Callbacks
A generic container needs four operations, each provided by the caller:
| Callback | Purpose | Signature |
|---|---|---|
build |
Create a data item | void *(*build)(FILE *fp) |
print |
Display a data item | void (*print)(void *data) |
compare |
Compare two items | int (*compare)(void *a, void *b) |
clean |
Free a data item | void (*clean)(void *data) |
The container calls these functions without knowing the type. Each callback casts the void * to the real type internally.
The Trick: Think of the generic container as a machine with four plugs. Different data types provide different plug implementations: ints plug in
print_int,compare_int, etc. Strings plug inprint_string,compare_string. Same machine, different plugs.
build, print, compare, and clean. Why does the container need a clean callback instead of just calling free(data)?free(data) only frees the outer allocation. If the data is a Word struct with char *letters, you need to free the string first, then the struct. The container doesn't know the internal structure — only the type-specific clean callback knows how to properly free everything.
Linked List Node
A generic linked list stores void * data:
typedef struct node
{
void *data;
struct node *next;
} Node;
Building the List
Node *prepend(Node *head, void *data)
{
Node *new_node = calloc(1, sizeof(Node));
if (new_node == NULL) return head;
new_node->data = data;
new_node->next = head;
return new_node;
}
Traversal with Callbacks
void print_list(Node *head, void (*print_fn)(void *))
{
Node *current = head;
while (current != NULL)
{
print_fn(current->data);
current = current->next;
}
}
Freeing the List
void free_list(Node *head, void (*clean_fn)(void *))
{
Node *current = head;
while (current != NULL)
{
Node *temp = current->next;
if (clean_fn != NULL)
{
clean_fn(current->data); // Free the data
}
free(current); // Free the node
current = temp;
}
}
Common Pitfall: When freeing a linked list, save
current->nextbefore freeingcurrent. If you free the node first, you lose thenextpointer — you can’t continue traversal. Use a temp variable.
free_list function, what happens if you write free(current); current = current->next; instead of saving next first?free(current), the memory at current is deallocated. Accessing current->next reads freed memory — this is undefined behavior. It might appear to work sometimes (the old data is still there) but will eventually cause crashes or data corruption. Always save current->next in a temp variable before freeing.
Type-Specific Callbacks
For integers:
void print_int(void *data)
{
int *p = (int *)data;
printf("%d\n", *p);
}
int compare_ints(void *a, void *b)
{
int *ia = (int *)a;
int *ib = (int *)b;
return *ia - *ib;
}
void clean_int(void *data)
{
free(data);
}
For a Word struct with a dynamically allocated string:
typedef struct
{
char *letters;
int length;
} Word;
void print_word(void *data)
{
Word *w = (Word *)data;
printf("%s (%d chars)\n", w->letters, w->length);
}
void clean_word(void *data)
{
Word *w = (Word *)data;
free(w->letters); // Free string first
free(w); // Then free struct
}
Key Insight: Each callback follows the same pattern: check for NULL, cast
void *to the real type, do the work. The generic container never knows what type it holds — it just calls whatever callbacks the user provides.
From Java: This is equivalent to Java’s
ArrayList<T>with customComparator,toString(), andclose()methods. Java achieves this with interfaces and generics (compile-time type safety). C achieves it withvoid *and function pointers (no type safety — you must get it right yourself).
Why does this matter?
The generic container pattern is the culmination of everything in Series 4. Structs give you data, function pointers give you pluggable behavior, void pointers give you type erasure. Combined, they produce reusable containers — the same code handles ints, strings, or any struct. This is exactly what Lab 8 asks you to build.
Deep dive: Tracing a callback through the generic container
Let’s trace exactly what happens when the generic container calls print_fn(current->data) on a Word:
Step 1: Container calls print_fn(current->data)
print_fn is a void (*)(void *)
current->data is a void * pointing to a Word struct
Step 2: print_fn resolves to print_word (the callback we plugged in)
Step 3: Inside print_word:
void print_word(void *data) ← data is the void * from step 1
{
Word *w = (Word *)data; ← cast to real type
printf("%s (%d chars)\n", w->letters, w->length);
}
Step 4: Memory layout being accessed:
┌──────────────┐
│ Node │
│ data ───────────→ ┌──────────┐
│ next ──→ ...│ │ Word │
└──────────────┘ │ letters ───→ "hello\0"
│ length = 5│
└──────────┘
The container sees: “I have a void *. I’ll call whatever function the user gave me.”
The callback sees: “I received a void *. I know it’s really a Word *, so I cast and use it.”
Neither side knows the other’s implementation. The container doesn’t know about Word. The callback doesn’t know about linked lists. They communicate through the void * interface. This is the same separation that Java’s interfaces provide — the container depends on an interface, not a concrete type.
Linked Lists vs. Arrays
| Feature | Array | Linked List |
|---|---|---|
| Random access | O(1) | O(n) |
| Insert at front | O(n) | O(1) |
| Memory layout | Contiguous | Scattered |
| Size | Fixed or realloc | Grows naturally |
| Cache performance | Excellent | Poor |
Quick Check: Why does the generic container use void *data instead of a specific type?
So it can store any type. The container doesn’t know or care what the data is — it stores a void * and delegates all type-specific operations (print, compare, clean) to callback functions that the user provides.
Quick Check: Why must you save current->next before freeing current in a linked list?
free(current) deallocates the node’s memory. After that, current->next reads freed memory (undefined behavior). Saving next in a temp variable before freeing ensures you can continue traversal.
Quick Check: Why does clean_word free w->letters before w?
w->letters points to separately allocated memory (the string). If you free w first, you lose access to w->letters — the string leaks. Always free inner allocations before the outer struct.
Big Picture: Generic containers are how C libraries achieve reusability without templates or generics. The Linux kernel’s linked list implementation, SQLite’s B-tree, and the C standard library’s
qsortall use this same pattern: void pointers for data, function pointers for operations. You’ve now built the same machinery that powers real-world systems software.
What’s Next
You’ve built the generic container pattern from scratch. In the next lesson, you’ll see how this applies to Lab 8 — implementing type-specific callbacks for a generic array that stores Word structs.