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 memcpy to read/write generic data — it works on raw bytes with no alignment issues
  • Pass size_t elem_size alongside void* 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
  • malloc returns maximally aligned memory — always safe to cast to any type
  • Generic data structures (stack, queue, vector) work with void* and element-size parameters