C doesn’t have templates or generics. But void* — a pointer to memory of unspecified type — fills that role. The standard library uses it in malloc, qsort, bsearch, and memcpy. Understanding void* is the key to writing reusable data structures in C.
The Basics
#include <stdio.h>
int main() {
int x = 42;
double d = 3.14;
char s[] = "hello";
void *ptr;
ptr = &x; /* any pointer type converts to void* implicitly */
ptr = &d;
ptr = s;
/* void* does NOT convert back implicitly — explicit cast needed */
int *ip = (int*)ptr; /* wrong if ptr doesn't point to int */
double *dp = (double*)ptr;
return 0;
}Any data pointer type can be assigned to void* without a cast. To use the memory, you cast it back to the correct type. If you cast to the wrong type, you get undefined behavior — C does not check this at runtime.
A Generic swap() Function
#include <string.h>
#include <stdio.h>
void swap(void *a, void *b, size_t size) {
unsigned char temp[256]; /* enough for most types */
memcpy(temp, a, size);
memcpy(a, b, size);
memcpy(b, temp, size);
}
int main() {
int x = 5, y = 10;
printf("Before: x=%d y=%dn", x, y);
swap(&x, &y, sizeof(int));
printf("After: x=%d y=%dn", x, y); /* x=10 y=5 */
double a = 1.1, b = 2.2;
swap(&a, &b, sizeof(double));
printf("Doubles: a=%.1f b=%.1fn", a, b); /* a=2.2 b=1.1 */
return 0;
}This is exactly how qsort swaps elements — it receives void* pointers to elements of unknown type and uses the size parameter to copy the right number of bytes. memcpy works on raw bytes regardless of type.
Generic Stack Using void*
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
unsigned char *data; /* raw byte storage */
size_t elem_size;
size_t count;
size_t capacity;
} GenStack;
GenStack *genstack_new(size_t elem_size) {
GenStack *s = malloc(sizeof(GenStack));
s->elem_size = elem_size;
s->count = 0;
s->capacity = 8;
s->data = malloc(8 * elem_size);
return s;
}
int genstack_push(GenStack *s, const void *elem) {
if (s->count == s->capacity) {
size_t new_cap = s->capacity * 2;
unsigned char *tmp = realloc(s->data, new_cap * s->elem_size);
if (!tmp) return -1;
s->data = tmp;
s->capacity = new_cap;
}
memcpy(s->data + s->count * s->elem_size, elem, s->elem_size);
s->count++;
return 0;
}
int genstack_pop(GenStack *s, void *out) {
if (s->count == 0) return -1;
s->count--;
memcpy(out, s->data + s->count * s->elem_size, s->elem_size);
return 0;
}
void genstack_free(GenStack *s) { free(s->data); free(s); }
int main() {
/* Stack of ints */
GenStack *si = genstack_new(sizeof(int));
int vals[] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) genstack_push(si, &vals[i]);
int v;
while (genstack_pop(si, &v) == 0) printf("%d ", v); /* 5 4 3 2 1 */
printf("n");
genstack_free(si);
/* Same stack, now holds doubles */
GenStack *sd = genstack_new(sizeof(double));
double dvals[] = {1.1, 2.2, 3.3};
for (int i = 0; i < 3; i++) genstack_push(sd, &dvals[i]);
double dv;
while (genstack_pop(sd, &dv) == 0) printf("%.1f ", dv); /* 3.3 2.2 1.1 */
printf("n");
genstack_free(sd);
return 0;
}Alignment — The Constraint void* Doesn’t Enforce
#include <stdint.h>
#include <stdio.h>
int main() {
char buffer[16];
void *p = buffer + 1; /* p is not 4-byte aligned */
/* Undefined behavior: reading int from unaligned address */
/* (may crash on ARM, may silently work on x86) */
int *ip = (int*)p;
/* *ip = 42; — UB on strict alignment platforms */
return 0;
}When casting from void* back to a typed pointer, the address must satisfy that type’s alignment requirement. malloc always returns maximally aligned memory, so this is safe. But if you partition a buffer manually and cast at offsets, you must ensure alignment — especially for types larger than char.
Understanding how pointers work in C deeply is the prerequisite for using void* correctly — the raw address arithmetic, alignment, and type system all interact. You can test generic functions in our c program compiler — verify that swap works correctly for different element types.
TL;DR
- Any pointer type converts to
void*implicitly; converting back requires an explicit cast - C does not check if the cast-back type matches what was originally stored — the programmer is responsible
- Use
memcpyto read/write generic data — it works on raw bytes with no alignment issues - Pass
size_t elem_sizealongsidevoid*to know how many bytes to copy - Cast from
void*to a typed pointer only when the original type is known and alignment is satisfied mallocreturns maximally aligned memory — always safe to cast to any type- Generic data structures (stack, queue, vector) work with
void*and element-size parameters