void pointers, NULL, and generic code
Pointers and memory12 min readLesson 3 of 6
3.1 An address without a type
void * holds the address of any object. It records where something is, not what type it has, so the compiler cannot size a dereference or scale arithmetic on it.
Because the type is absent, a void * can travel through code that does not care about the payload, and the type comes back at the edge where it is known.
C converts between void * and other object pointer types implicitly in assignment. A cast is optional, and writing one makes the intended type visible to the next reader.
#include <stdio.h>
int main(void)
{
int number = 42;
double ratio = 2.5;
void *slot = &number;
printf("int: %d\n", *(int *)slot);
slot = ∶
printf("double: %.1f\n", *(double *)slot);
return 0;
}Output
int: 42 double: 2.5
3.2 A swap that does not know the type
Byte-wise copying with unsigned char * makes a swap work for any object size. The function takes two void * values and a size, then exchanges one byte at a time.
The compiler cannot compute a + 1 on a void *, so convert to unsigned char * first. Character types may alias any object type, which makes this loop well defined.
The caller supplies sizeof of the actual objects. The size has to match both arguments, because a mismatch writes past one of them.
#include <stdio.h>
static void swap_any(void *a, void *b, size_t size)
{
unsigned char *pa = a;
unsigned char *pb = b;
for (size_t i = 0; i < size; i++) {
unsigned char temp = pa[i];
pa[i] = pb[i];
pb[i] = temp;
}
}
int main(void)
{
int x = 3, y = 9;
swap_any(&x, &y, sizeof x);
printf("%d %d\n", x, y);
return 0;
}Output
9 3
3.3 The standard library uses the same idea
qsort sorts any array because its interface is built from void * and sizes. The comparison callback receives two const void * values that point at elements, and it casts them to the real element type.
The callback returns a negative value, zero, or a positive value, and qsort orders the elements by that sign. Subtracting two int values seems shorter, but the difference can overflow; subtracting the two boolean results avoids that.
This is the pattern for generic code in C: pass the address, pass the size, and pass a function that knows the type.
#include <stdio.h>
#include <stdlib.h>
static int compare_ints(const void *left, const void *right)
{
int a = *(const int *)left;
int b = *(const int *)right;
return (a > b) - (a < b);
}
int main(void)
{
int values[6] = {42, 7, 19, 3, 88, 21};
qsort(values, 6, sizeof values[0], compare_ints);
for (int i = 0; i < 6; i++) {
printf("%d\n", values[i]);
}
return 0;
}Output
3 7 19 21 42 88
3.4 Null checks at the boundary
NULL is a null pointer constant, and <stddef.h> defines it. Several other headers define it too, including <stdio.h>, so a program that already includes one of them usually has it.
A pointer that may lack a value should be NULL. Functions that fail return NULL, and a caller that ignores that result writes through an address that belongs to nothing.
Checking at the point of use is cheap. A function can also reject bad arguments early and return an error code, which keeps the check in one place.
#include <stdio.h>
static int *find_value(int *values, int count, int target)
{
for (int i = 0; i < count; i++)
if (values[i] == target)
return values + i;
return NULL;
}
int main(void)
{
int values[4] = {5, 8, 13, 21};
int *found = find_value(values, 4, 13);
if (found != NULL) {
printf("found at index %td\n", found - values);
}
printf("missing is NULL: %d\n", find_value(values, 4, 99) == NULL);
return 0;
}Output
found at index 2 missing is NULL: 1
3.5 NULL is not an empty string
NULL is a pointer value. '\0' is the character with value 0 that ends a string. An empty string "" is a valid array containing only that terminator.
Passing NULL where a function expects a string is a common bug. The function calls strlen, which dereferences the pointer, and the program stops.
Guard the edge with if (text == NULL) { return 0; } at the top of a function that accepts an optional string. A pointer to "" needs no guard, because the terminator is already there.
#include <stdio.h>
#include <string.h>
static size_t safe_length(const char *text)
{
if (text == NULL) {
return 0;
}
return strlen(text);
}
int main(void)
{
const char *empty = "";
printf("empty string: %zu\n", safe_length(empty));
printf("null pointer: %zu\n", safe_length(NULL));
return 0;
}Output
empty string: 0 null pointer: 0
3.6 const void * for read-only generic input
A generic function that only reads should take const void *. The const applies to the data behind the pointer, so the function cannot write through it without discarding the qualifier.
The caller can pass any object address. Inside the function, the cast to the real pointer type must include const as well: const int *p = (const int *)input;.
Dropping const in the cast compiles with a warning and invites a write to something declared read-only. Keep the qualifier through every step of the conversion.
#include <stdio.h>
static int same_bytes(const void *a, const void *b, size_t size)
{
const unsigned char *pa = (const unsigned char *)a;
const unsigned char *pb = (const unsigned char *)b;
for (size_t i = 0; i < size; i++) {
if (pa[i] != pb[i]) {
return 0;
}
}
return 1;
}
int main(void)
{
int x = 7, y = 7, z = 8;
printf("%d %d\n", same_bytes(&x, &y, sizeof x), same_bytes(&x, &z, sizeof x));
return 0;
}Output
1 0
3.7 Casts that hide mistakes
A void * accepts any object address, so a wrong cast is not caught until run time. Casting an int * to double * and reading through it produces garbage or an alignment fault.
Alignment matters for the target type: a double * is expected on a boundary that suits the type, and a pointer converted from a char buffer may not have that boundary.
Function pointers are a separate case. The C standard does not guarantee that converting a function pointer to void * preserves it, so keep function pointer types exact.
#include <stdio.h>
#include <string.h>
#include <stdint.h>
int main(void)
{
uint32_t bits = 0x3FC00000u;
float number = 0.0f;
memcpy(&number, &bits, sizeof number);
printf("%.1f\n", number);
return 0;
}Output
1.5
