C in 100 Seconds: Void Pointers | Episode 30
Video: C in 100 Seconds: Void Pointers — Generic Pointers in C | Episode 30 by Taught by Celeste AI - AI Coding Coach
C Void Pointers: Generic Pointers
void *ptrholds any pointer type. Cast back to a specific type before dereferencing. The mechanism behindqsort,malloc, and generic data structures.
void * is C's "pointer of unknown type." It's how you write code that handles multiple types — generic containers, callbacks, allocators.
The basic shape
#include <stdio.h>
void print_value(void *ptr, char type) {
if (type == 'i')
printf("int: %d\n", *(int *)ptr);
else if (type == 'f')
printf("float: %.2f\n", *(float *)ptr);
else if (type == 'c')
printf("char: %c\n", *(char *)ptr);
}
int main() {
int x = 42;
float y = 3.14;
char z = 'A';
void *ptr;
ptr = &x;
printf("int: %d\n", *(int *)ptr);
ptr = &y;
printf("float: %.2f\n", *(float *)ptr);
ptr = &z;
printf("char: %c\n", *(char *)ptr);
print_value(&x, 'i');
print_value(&y, 'f');
print_value(&z, 'c');
return 0;
}
Implicit conversions
int x = 5;
void *p = &x; // OK — any pointer → void *
int *back = p; // OK — void * → typed pointer (in C; C++ requires cast)
In C, conversions to/from void * are automatic — no cast required. (C++ is stricter; you need a cast.)
For clarity, many C programmers cast anyway:
int *back = (int *)p;
Can't dereference directly
void *p = &x;
int y = *p; // ERROR — can't dereference void *
*p is meaningless without a type — the compiler doesn't know how many bytes to read or how to interpret them. You must cast first:
int y = *(int *)p; // OK — read 4 bytes as int
Same for indexing or arithmetic — void * doesn't support either.
qsort: the generic sort
#include <stdlib.h>
int compare_ints(const void *a, const void *b) {
int ia = *(int *)a;
int ib = *(int *)b;
return ia - ib;
}
int nums[] = {3, 1, 4, 1, 5, 9, 2, 6};
qsort(nums, 8, sizeof(int), compare_ints);
qsort doesn't know the type of your array. You provide:
- Pointer to first element (any type — qsort treats as
void *). - Number of elements.
- Size of each element (
sizeof(int)here). - Comparator function that takes two
void *and returns negative/0/positive.
Inside the comparator, cast the void * to your real type before reading.
bsearch: binary search
int target = 5;
int *found = bsearch(&target, sorted_arr, n, sizeof(int), compare_ints);
if (found) printf("Found at index %ld\n", found - sorted_arr);
Same generic pattern. Returns a void * pointing to the matching element, or NULL.
malloc returns void*
int *p = malloc(sizeof(int)); // void * implicitly converted to int *
char *s = malloc(100); // same
The void * return is what makes malloc work for any type. The implicit conversion (or explicit cast in C++) lets you assign to your typed pointer.
Generic data structures
typedef struct Node {
void *data;
struct Node *next;
} Node;
Node *head = malloc(sizeof(Node));
int x = 42;
head->data = &x;
// retrieve
int y = *(int *)head->data;
A linked list that holds any type. The cost: you have to remember what type each node holds. C doesn't track it for you.
For type-safe generics, C++ has templates; C either uses void* + manual type tracking, or _Generic macros (C11+):
#define print(x) _Generic((x), \
int: print_int, \
double: print_double, \
char *: print_string \
)(x)
_Generic selects the function at compile time based on the argument's type. Cleaner than void* + cast for some cases.
void p vs char p
For byte-by-byte access:
void *vp = some_buffer;
char *cp = (char *)vp;
cp[0] // first byte
cp[1] // second byte
vp[0] // ERROR — can't index void *
char * supports arithmetic and indexing in single bytes. void * doesn't (technically — GCC has an extension that lets it work, but it's not standard).
For "walk this memory byte-by-byte," use char * (or unsigned char *).
Pointer size considerations
sizeof(void *) // 8 on 64-bit, 4 on 32-bit
sizeof(int *) // same as void *
sizeof(int(*)()) // function pointer; usually same, but not guaranteed
All data pointers are the same size on a given platform. Function pointers may differ on weird architectures (Harvard architectures have separate code/data pointers).
POSIX guarantees sizeof(void *) == sizeof(void(*)()), which lets dlsym (dynamic library symbol lookup) return void * for both data and functions.
Common mistakes
Dereferencing void * without cast. Compiler error. Always cast first.
Wrong type cast. *(double *)p when p actually points to an int — reads past the int into adjacent memory. Garbage.
Pointer arithmetic on void *. Doesn't compile in standard C. Cast to char *.
Forgetting to track type for generic structures. Storing void * data without knowing what type it is = bug factory. Either commit to a single type per container, or store a type tag alongside.
qsort comparator returning a - b for floats. 0.5 - 0.4 = 0.1, but the function returns int, so 0.1 becomes 0. Use explicit if/else for floats:
int compare_doubles(const void *a, const void *b) {
double da = *(double *)a;
double db = *(double *)b;
if (da < db) return -1;
if (da > db) return 1;
return 0;
}
What's next
Episode 31: stack vs heap. Where variables actually live. Why returning a pointer to a local is a disaster.
Recap
void * is the "any pointer" type. Implicit conversion (in C) to and from typed pointers. Cannot dereference, index, or do arithmetic directly — cast to a specific type first. Used by qsort, bsearch, malloc, and any generic API. Pair with manual type tracking for generic data structures, or use _Generic for type-dispatched macros (C11+). For byte-level access, use char *.
Next episode: stack vs heap.