C / POINTERS
void pointers and generic data handling
Write generic C functions that take data through void *, supply the size or type tag they need, and cast back to the real type without breaking anything.
What you will learn
- Convert any object pointer to void * and back to its original type losslessly
- Pair every void * with a size_t or a type tag, since the pointer carries no type
- Read and copy generic bytes through unsigned char * and memcpy
- Write qsort comparators that cast const void * to the real element type
Understanding void pointers and generic data handling
A void * is an address with the type stripped off. The standard guarantees that a pointer to any object can be converted to void * and back to its original type unchanged, which is what makes it C's universal handle for data of unknown kind. What you lose is everything else: void is an incomplete type with no size and no representation, so *p, p[0] and p + 1 are all illegal on a void *. The compiler is not being fussy here; it genuinely has no answer to how many bytes to read or how to interpret them.
Think of a typed pointer as carrying two facts: where the object is, and how to read and step over it. A void * keeps only the first, so every generic interface has to hand you the second one out of band. That is why memcpy and memset take a byte count, why qsort takes an element size as well as an element count, and why a generic container stores a type tag or a function pointer next to the data. The cast back to a concrete type is a promise you make to the compiler, and nobody checks it.
In practice, convert once at the top of the function and work through a typed pointer afterwards. For byte-level work that type is unsigned char *, the one type allowed to inspect the raw representation of any object, which is why generic copying goes through memcpy rather than assignment. Conversions to and from void * are implicit in C, so malloc's return value needs no cast and passing &x to a void * parameter needs none either; the explicit cast reappears only at the dereference, because that is the point where you assert the type again.
<stdio.h>
<string.h>
/* void * gives us the address but not the type, so the caller has to
supply the size separately. */
static void swap(void *a, void *b, size_t size)
{
unsigned char tmp[16];
unsigned char *pa = a; /* no cast needed: void * converts implicitly */
unsigned char *pb = b;
if (size > sizeof tmp)
return; /* refuse objects bigger than the buffer */
memcpy(tmp, pa, size);
memcpy(pa, pb, size);
memcpy(pb, tmp, size);
}
int main(void)
{
int i = 3, j = 7;
double x = 1.5, y = -2.25;
char s[] = "AB";
swap(&i, &j, sizeof i);
swap(&x, &y, sizeof x);
swap(&s[0], &s[1], sizeof s[0]);
printf("ints : %d %d\n", i, j);
printf("doubles : %g %g\n", x, y);
printf("string : %s\n", s);
void *hidden = &i; /* the int type is erased here */
printf("recovered: %d\n", *(int *)hidden);
return 0;
}
A void * keeps an object's address but discards its type, so any generic interface must carry the missing size or type information alongside the pointer.
Worked examples
A qsort comparator
The standard library's generic sort works entirely through void *, so the comparison function has to restore the type itself.
<stdio.h>
<stdlib.h>
static int cmp_int(const void *a, const void *b)
{
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y);
}
int main(void)
{
int v[] = { 42, -1, 17, 0, 8 };
size_t n = sizeof v / sizeof v[0];
qsort(v, n, sizeof v[0], cmp_int);
for (size_t k = 0; k < n; k++)
printf("%s%d", k ? ", " : "", v[k]);
putchar('\n');
return 0;
}
Example explained
Line 1qsort passes two const void *, so cmp_int casts each one to const int * before dereferencing it.
Line 2(x > y) - (x < y) gives -1, 0 or 1 without the overflow that x - y risks for large values.
Line 3sizeof v[0] is qsort's stride; it cannot be recovered from the void * it was handed.
Line 4The count n and the element size are separate arguments because the pointer supplies neither.
Tagged generic values
Storing a type tag next to a void * is how you carry different kinds of data in one array.
<stdio.h>
enum kind { K_INT, K_DOUBLE, K_STR };
struct value {
enum kind kind;
void *data;
};
static void print_value(struct value v)
{
switch (v.kind) {
case K_INT: printf("int %d\n", *(int *)v.data); break;
case K_DOUBLE: printf("double %.2f\n", *(double *)v.data); break;
case K_STR: printf("str %s\n", (char *)v.data); break;
}
}
int main(void)
{
int n = 12;
double d = 3.5;
char msg[] = "hi";
struct value list[] = {
{ K_INT, &n },
{ K_DOUBLE, &d },
{ K_STR, msg }
};
for (size_t i = 0; i < sizeof list / sizeof list[0]; i++)
print_value(list[i]);
return 0;
}
Example explained
Line 1struct value pairs the erased pointer with an enum tag, putting back the type information void * dropped.
Line 2Each case casts v.data to the type the tag promises, and only then dereferences it.
Line 3K_STR needs no dereference: msg already decayed to char *, so the cast alone is enough for %s.
Line 4Storing &n and &d in the void * member needs no cast, because conversion to void * is implicit in C.
Important notes
Arithmetic on a void * is a GCC and Clang extension that steps one byte at a time; standard C forbids it and sizeof(void) is invalid, so cast to unsigned char * first.
void * is guaranteed only for object pointers. Function pointers are a separate family, so keep them in a function pointer type such as void (*)(void), not in a void *.
Common mistakes
Dereferencing or indexing a void * directly with *p or p[i]: standard C rejects it, and where GCC's extension allows p + 1 it steps a single byte instead of one element.
Casting back to the wrong type, for example storing a double * and reading *(int *)p: it compiles without a warning and gives garbage, because the cast is an assertion the compiler cannot verify.
Passing sizeof v or the size of the pointer instead of sizeof v[0] to qsort or a memcpy-based helper, which makes it stride and copy the wrong number of bytes and overwrite neighbouring objects.
Try it yourself
Change, predict, then run
Write void print_bytes(const void *p, size_t n) that casts its argument to const unsigned char * and prints n bytes in hex. Call it on an int holding 1 and on a float holding 1.0f, and compare the two byte patterns.
Open the C workspaceCheck your understanding
qsort already receives a pointer to the array and a comparison function. Why does it still need the element size as a separate argument?
- Because a void * records only an address, so qsort cannot know how far apart consecutive elements are or how many bytes to exchange
- Because the array decayed to a pointer, so qsort has lost the number of elements it must sort
- Because the comparison function receives const void * and is therefore unable to apply sizeof itself
- Because sizeof is evaluated at compile time and library functions are not allowed to use it
Show answer
qsort has to index through the array and swap raw bytes, and the stride for both operations lives in the pointer's type, which is exactly what void * discards. The 'lost length' answer is wrong because the element count is already supplied as the separate nmemb argument; the fact missing from the pointer is the size of one element, not how many there are.