advanced-c Lesson 7 20 min read

How Do Void Pointers Enable Generic Programming?

void * as the universal pointer — type erasure, casting, and why qsort works on any type

Reading: C Text: Ch. 13 §1 (pp. 727–740, Void Pointers)

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 qsort uses void * and comparators to sort any type
  • Compare void * in C to Java’s Object type

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 a void *, 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; then double *dp = (double *)ptr; printf("%f", *dp); interprets the int’s bytes as a double — nonsense output. There’s no ClassCastException in C. You’re on your own.

Check Your Understanding
You have int x = 42; void *ptr = &x;. Which line correctly prints 42?
A printf("%d", *ptr);
B printf("%d", *(int *)ptr);
C printf("%d", (int)ptr);
D printf("%d", ptr);
Answer: B. You can't dereference a 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 of Object in Java. Java’s Object carries runtime type information (you can call instanceof). C’s void * carries nothing — no type info, no runtime checks. Java catches wrong casts with ClassCastException; C gives you garbage. The power and the danger are both greater.

Check Your Understanding
Why can't you do pointer arithmetic on a void *?
A The compiler doesn't know the size of the pointed-to type, so it can't calculate how many bytes to skip
B void * can only be used with malloc and free
C void * always points to read-only memory
D Pointer arithmetic is only allowed on arrays, and void arrays don't exist
Answer: A. When you write 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.