Part of C in 100s

C in 100 Seconds: Void Pointers | Episode 30

Celest KimCelest Kim

Video: C in 100 Seconds: Void Pointers — Generic Pointers in C | Episode 30 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

C Void Pointers: Generic Pointers

void *ptr holds any pointer type. Cast back to a specific type before dereferencing. The mechanism behind qsort, 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.