How Do Void Pointers Enable Generic Programming?
void * as the universal pointer — type erasure, casting, and why qsort works on any type
After this lesson, you will be able to:
- Explain that
void *is typeless and must be cast before dereferencing - Write generic functions that accept
void *parameters - Explain how
qsortusesvoid *and comparators to sort any type - Compare
void *in C to Java’sObjecttype
The Problem
Your sort function works with Student structs. But what if you want a sort function that works with any type — ints, doubles, strings, structs? You’d need a way to say “this pointer can point to anything.” That’s void *.
The Universal Pointer
void * Points to Anything
A void * is a pointer with no type information:
int x = 42;
double y = 3.14;
char *s = "hello";
void *ptr;
ptr = &x; // OK — void * accepts any pointer
ptr = &y; // OK
ptr = s; // OK
You’ve already used void * without knowing it — calloc and malloc return void *:
int *arr = calloc(10, sizeof(int)); // calloc returns void *, implicitly cast to int *
Casting Back
To use the data through a void *, you must cast it back to the correct type:
void *ptr = &x;
int *ip = (int *)ptr; // Cast back to int *
printf("%d\n", *ip); // 42
Key Insight:
void *is type erasure — it forgets what type the pointer points to. You can store any pointer in avoid *, but to use the data, you must cast it back to the right type. If you cast to the wrong type, you get garbage — silently, with no error.
Common Pitfall: Casting to the wrong type produces garbage silently.
void *ptr = &x;thendouble *dp = (double *)ptr; printf("%f", *dp);interprets the int’s bytes as a double — nonsense output. There’s noClassCastExceptionin C. You’re on your own.
int x = 42; void *ptr = &x;. Which line correctly prints 42?void * directly — the compiler doesn't know the type. Cast it to int * first, then dereference: *(int *)ptr. Option A tries to dereference void * directly (compile error). Option C casts the address itself to int, printing the memory address as a number. Option D prints the raw pointer value.
No Pointer Arithmetic on void *
Since void has no size, you can’t do ptr + 1 on a void * — the compiler doesn’t know how many bytes to skip. Cast first:
void *ptr = arr;
// ptr + 1; // ERROR or undefined
int *ip = (int *)ptr;
ip + 1; // OK — moves by sizeof(int)
Writing Generic Functions
void print_int(void *data)
{
int *p = (int *)data;
printf("%d", *p);
}
void print_double(void *data)
{
double *p = (double *)data;
printf("%.2f", *p);
}
void print_string(void *data)
{
char **p = (char **)data;
printf("%s", *p);
}
Each function takes void *, casts to the expected type, and does its work. The caller chooses which function to pass.
From Java:
void *is C’s version ofObjectin Java. Java’sObjectcarries runtime type information (you can callinstanceof). C’svoid *carries nothing — no type info, no runtime checks. Java catches wrong casts withClassCastException; C gives you garbage. The power and the danger are both greater.
void *?ptr + 1 on an int *, the compiler adds sizeof(int) bytes. With void *, sizeof(void) is undefined — the compiler doesn't know how far to advance. You must cast to a typed pointer first to make arithmetic meaningful.
Why does this matter?
void * is the mechanism behind every generic C API. When you call qsort, bsearch, or build a generic linked list, void * is what makes it possible to handle any type. Understanding its limitations — no dereferencing, no arithmetic, no type safety — is the key to using it correctly.
Deep dive: The four-callback pattern for generic containers
In Lab 8, you’ll build a generic container that stores any type. The pattern uses void * for data storage and four function pointer callbacks for type-specific operations:
| Callback | Purpose | Java equivalent |
|---|---|---|
buildType |
Create/parse a new element | Constructor |
printType |
Display an element | toString() |
compareType |
Compare two elements | compareTo() / Comparator |
cleanType |
Free an element’s memory | Garbage collection (automatic in Java) |
typedef void *(*BuildFunc)(void);
typedef void (*PrintFunc)(void *);
typedef int (*CompareFunc)(const void *, const void *);
typedef void (*CleanFunc)(void *);
The container itself stores void * data and these four function pointers. It doesn’t know or care what type of data it holds — it calls the callbacks whenever it needs to create, print, compare, or free elements. You swap in a different set of four callbacks to make the same container work with a different type.
This is exactly how Java generics work under the hood — except Java uses type erasure at compile time and runtime type checks, while C uses void * and trusts you to get the casts right.
How qsort Uses void *
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
qsort doesn’t know the type — it works with raw bytes. The comparison function casts the void * arguments to the correct type:
int compare_ints(const void *a, const void *b)
{
const int *ia = (const int *)a;
const int *ib = (const int *)b;
if (*ia < *ib) return -1;
if (*ia > *ib) return 1;
return 0;
}
Quick Check: Why can't you dereference a void * directly?
The compiler doesn’t know the type, so it doesn’t know how many bytes to read or how to interpret them. You must cast to a typed pointer first: *(int *)ptr reads 4 bytes as an integer.
Quick Check: What happens if you cast a void * to the wrong type?
You get garbage data — the bytes are reinterpreted according to the wrong type. C has no runtime type checking, so there’s no error or exception. This is one of the most dangerous bugs in generic C code.
Quick Check: How is void * different from Java's Object?
Java’s Object carries runtime type information — you can use instanceof and get ClassCastException on wrong casts. C’s void * carries no type information. Wrong casts produce silent garbage.
Big Picture: Void pointers are C’s type erasure mechanism — they let you write code that works with data it doesn’t understand. Combined with function pointers (which provide the type-specific operations), you get generic programming: one container that stores anything, one sort that orders anything. This is what Java generics automate; in C, you build the machinery yourself.
What’s Next
You have two powerful tools: function pointers (parameterize behavior) and void pointers (parameterize type). Combined, they let you build generic data structures — containers that store any type and operate on it through callbacks. That’s the next lesson: building C’s version of Java generics.