C Programming: Low-Level Mastery
Pointers

Void Pointers

Master void pointers (void *) - C's generic pointer type. Learn type-agnostic programming, malloc return type, generic functions, and when to use void pointers for maximum flexibility.

Understanding Void Pointers

A void pointer (void *) is a generic pointer that can point to any data type. It's a "pointer to unknown type" - the compiler doesn't know what it points to. You can't dereference a void pointer directly; you must cast it to a specific type first. Void pointers enable generic programming in C, allowing functions to work with any data type.

C
#include <stdio.h>

int main(void) {
    int x = 42;
    double y = 3.14;
    char z = 'A';
    
    /* Void pointer can point to any type */
    void *ptr;
    
    ptr = &x;  // Points to int
    printf("int: %d\n", *(int*)ptr);  // Cast to int* before dereferencing
    
    ptr = &y;  // Now points to double
    printf("double: %f\n", *(double*)ptr);  // Cast to double*
    
    ptr = &z;  // Now points to char
    printf("char: %c\n", *(char*)ptr);  // Cast to char*
    
    return 0;
}

/* Key points:
   - void* can point to any type
   - Cannot dereference without casting
   - Cannot do pointer arithmetic (no known size)
   - Any pointer can be assigned to void*
*/

Common Uses of Void Pointers

Void pointers appear throughout C's standard library and are essential for generic programming. Understanding when and how to use them enables writing flexible, reusable code.

malloc and Memory Allocation

The most common void pointer: malloc returns void* because it doesn't know what type you're allocating. This lets malloc work for any type. You cast the result to the appropriate type.

C
#include <stdlib.h>

/* malloc returns void* */
int *int_array = malloc(10 * sizeof(int));     // Implicitly cast to int*
double *dbl_array = malloc(5 * sizeof(double)); // Cast to double*
char *string = malloc(100 * sizeof(char));      // Cast to char*

/* Explicit cast (optional in C, required in C++) */
int *arr = (int*)malloc(10 * sizeof(int));

/* Generic allocation wrapper */
void* allocate_memory(size_t size) {
    void *ptr = malloc(size);
    if (ptr == NULL) {
        fprintf(stderr, "Allocation failed\n");
        exit(1);
    }
    return ptr;
}

/* Usage */
int *numbers = allocate_memory(10 * sizeof(int));
double *values = allocate_memory(5 * sizeof(double));

/* Free works with void* too */
free(numbers);   // Takes void*
free(values);

/* memcpy, memset work with void* */
void *dest = malloc(100);
void *src = malloc(100);
memcpy(dest, src, 100);  // Both are void*
memset(dest, 0, 100);    // Takes void*

Generic Functions

Void pointers enable generic functions that work with any data type. The function receives void pointers and casts them internally based on context or a size parameter.

C
/* Generic swap function */
void swap(void *a, void *b, size_t size) {
    unsigned char *p = a;
    unsigned char *q = b;
    unsigned char temp;
    
    for (size_t i = 0; i < size; i++) {
        temp = p[i];
        p[i] = q[i];
        q[i] = temp;
    }
}

/* Usage with different types */
void swap_examples(void) {
    int x = 10, y = 20;
    swap(&x, &y, sizeof(int));
    printf("x=%d, y=%d\n", x, y);  // x=20, y=10
    
    double a = 1.5, b = 2.5;
    swap(&a, &b, sizeof(double));
    printf("a=%f, b=%f\n", a, b);  // a=2.5, b=1.5
}

/* Generic comparison function (like qsort) */
int compare_ints(const void *a, const void *b) {
    const int *ia = (const int*)a;
    const int *ib = (const int*)b;
    return (*ia &gt; *ib) - (*ia < *ib);
}

int compare_doubles(const void *a, const void *b) {
    const double *da = (const double*)a;
    const double *db = (const double*)b;
    return (*da &gt; *db) - (*da < *db);
}

/* Using qsort with void* */
void sort_example(void) {
    int arr[] = {5, 2, 8, 1, 9};
    qsort(arr, 5, sizeof(int), compare_ints);
    
    double darr[] = {3.14, 1.41, 2.71};
    qsort(darr, 3, sizeof(double), compare_doubles);
}

/* Generic print function */
void print_array(void *arr, size_t count, size_t elem_size,
                 void (*print_elem)(void*)) {
    unsigned char *ptr = arr;
    for (size_t i = 0; i < count; i++) {
        print_elem(ptr + i * elem_size);
    }
}

void print_int(void *p) {
    printf("%d ", *(int*)p);
}

void print_double(void *p) {
    printf("%f ", *(double*)p);
}

/* Usage */
void generic_print_example(void) {
    int ints[] = {1, 2, 3, 4, 5};
    print_array(ints, 5, sizeof(int), print_int);
    printf("\n");
    
    double doubles[] = {1.1, 2.2, 3.3};
    print_array(doubles, 3, sizeof(double), print_double);
    printf("\n");
}

Generic Data Structures

Void pointers enable generic data structures that can store any type. Linked lists, trees, and hash tables often use void* for the data payload, making them reusable for different data types.

C
/* Generic linked list node */
typedef struct Node {
    void *data;          // Can store any type
    struct Node *next;
} Node;

/* Generic list operations */
Node* list_create_node(void *data) {
    Node *node = malloc(sizeof(Node));
    if (node == NULL) return NULL;
    
    node-&gt;data = data;
    node-&gt;next = NULL;
    return node;
}

void list_insert(Node **head, void *data) {
    Node *new_node = list_create_node(data);
    if (new_node == NULL) return;
    
    new_node-&gt;next = *head;
    *head = new_node;
}

void list_free(Node *head, void (*free_data)(void*)) {
    while (head != NULL) {
        Node *temp = head;
        head = head-&gt;next;
        
        if (free_data != NULL) {
            free_data(temp-&gt;data);
        }
        free(temp);
    }
}

/* Usage with different types */
void list_example(void) {
    Node *int_list = NULL;
    
    /* Store integers */
    int *val1 = malloc(sizeof(int));
    *val1 = 42;
    list_insert(&int_list, val1);
    
    int *val2 = malloc(sizeof(int));
    *val2 = 100;
    list_insert(&int_list, val2);
    
    /* Access data */
    Node *current = int_list;
    while (current != NULL) {
        printf("%d ", *(int*)current-&gt;data);
        current = current-&gt;next;
    }
    printf("\n");
    
    list_free(int_list, free);
}

/* Generic stack */
typedef struct {
    void **items;     // Array of void pointers
    size_t size;
    size_t capacity;
} Stack;

Stack* stack_create(size_t capacity) {
    Stack *s = malloc(sizeof(Stack));
    if (s == NULL) return NULL;
    
    s-&gt;items = malloc(capacity * sizeof(void*));
    if (s-&gt;items == NULL) {
        free(s);
        return NULL;
    }
    
    s-&gt;size = 0;
    s-&gt;capacity = capacity;
    return s;
}

int stack_push(Stack *s, void *item) {
    if (s-&gt;size >= s-&gt;capacity) {
        return 0;  // Full
    }
    
    s-&gt;items[s-&gt;size++] = item;
    return 1;
}

void* stack_pop(Stack *s) {
    if (s-&gt;size == 0) {
        return NULL;  // Empty
    }
    
    return s-&gt;items[--s-&gt;size];
}

void stack_free(Stack *s) {
    free(s-&gt;items);
    free(s);
}

Restrictions on Void Pointers

Void pointers have limitations because the compiler doesn't know the pointed-to type. Understanding these restrictions prevents errors and helps you use void pointers correctly.

C
/* Restriction 1: Cannot dereference */
void *ptr = malloc(sizeof(int));
// printf("%d\n", *ptr);  // ERROR: Cannot dereference void*

/* Must cast first */
printf("%d\n", *(int*)ptr);  // OK

/* Restriction 2: Cannot do pointer arithmetic */
void *vp = malloc(10 * sizeof(int));
// vp++;  // ERROR: Compiler doesn't know size
// vp += 5;  // ERROR

/* Must cast to specific type */
int *ip = (int*)vp;
ip++;  // OK: Moves by sizeof(int)

/* Or cast for arithmetic */
vp = (char*)vp + 10;  // Moves by 10 bytes

/* Restriction 3: sizeof on void* gives pointer size, not data size */
void *ptr2 = malloc(100);
printf("%zu\n", sizeof(ptr2));  // 8 (pointer size), not 100

/* Restriction 4: Cannot create array of void */
// void arr[10];  // ERROR: Incomplete type

/* Can create array of void pointers */
void *ptr_array[10];  // OK

/* Restriction 5: Cannot have void function parameters */
// void func(void param) {"{ }"}  // ERROR

/* But can have void* parameters */
void func(void *param) {"{ }"}  // OK

/* What void* CAN do */

// 1. Be assigned any pointer type
int x = 10;
double y = 3.14;
void *vptr;
vptr = &x;  // OK
vptr = &y;  // OK

// 2. Be compared
void *p1 = malloc(100);
void *p2 = malloc(100);
if (p1 == p2) {"{ }"}  // OK
if (p1 < p2) {"{ }"}   // OK (implementation-defined)

// 3. Be assigned to any pointer type
vptr = malloc(sizeof(int));
int *ip2 = vptr;  // OK (implicit cast in C)

// 4. Be passed to functions
void process(void *data);
process(&x);  // OK

// 5. Be returned from functions
void* allocate(size_t size) {
    return malloc(size);
}

Safe Void Pointer Patterns

Using void pointers safely requires discipline. Follow these patterns to avoid crashes and undefined behavior.

C
/* Pattern 1: Always track type information */
typedef enum {
    TYPE_INT,
    TYPE_DOUBLE,
    TYPE_STRING
} DataType;

typedef struct {
    void *data;
    DataType type;
} TypedData;

void print_typed_data(TypedData *td) {
    switch (td-&gt;type) {
        case TYPE_INT:
            printf("%d\n", *(int*)td-&gt;data);
            break;
        case TYPE_DOUBLE:
            printf("%f\n", *(double*)td-&gt;data);
            break;
        case TYPE_STRING:
            printf("%s\n", (char*)td-&gt;data);
            break;
    }
}

/* Pattern 2: Use size parameter */
void copy_data(void *dest, const void *src, size_t size) {
    memcpy(dest, src, size);  // Size ensures correct copy
}

/* Pattern 3: Document expected type */
/**
 * Processes data. Expects void* pointing to int array.
 * @param data Pointer to int array
 * @param count Number of integers
 */
void process_ints(void *data, size_t count) {
    int *arr = (int*)data;
    for (size_t i = 0; i < count; i++) {
        arr[i] *= 2;
    }
}

/* Pattern 4: Null check before casting */
void safe_process(void *data) {
    if (data == NULL) {
        return;
    }
    
    int *ptr = (int*)data;
    // Use ptr...
}

/* Pattern 5: Use type-safe wrappers */
typedef struct {
    void *data;
    size_t size;
} Buffer;

Buffer* buffer_create(size_t size) {
    Buffer *buf = malloc(sizeof(Buffer));
    if (buf == NULL) return NULL;
    
    buf-&gt;data = malloc(size);
    if (buf-&gt;data == NULL) {
        free(buf);
        return NULL;
    }
    
    buf-&gt;size = size;
    return buf;
}

void* buffer_get_data(Buffer *buf) {
    return buf-&gt;data;
}

size_t buffer_get_size(Buffer *buf) {
    return buf-&gt;size;
}

/* Pattern 6: Const correctness with void* */
void read_only_process(const void *data, size_t size) {
    const unsigned char *bytes = (const unsigned char*)data;
    for (size_t i = 0; i < size; i++) {
        printf("%02X ", bytes[i]);
    }
}

/* Pattern 7: Alignment concerns */
void* aligned_alloc_wrapper(size_t size, size_t alignment) {
    void *ptr = aligned_alloc(alignment, size);
    if (ptr == NULL) {
        return malloc(size);  // Fallback
    }
    return ptr;
}

/* Pattern 8: Generic callback with context */
typedef void (*GenericCallback)(void *data, void *context);

void foreach_item(void **items, size_t count, 
                 GenericCallback callback, void *context) {
    for (size_t i = 0; i < count; i++) {
        callback(items[i], context);
    }
}

void print_with_prefix(void *data, void *context) {
    const char *prefix = (const char*)context;
    int value = *(int*)data;
    printf("%s%d\n", prefix, value);
}

/* Usage */
void callback_example(void) {
    int a = 1, b = 2, c = 3;
    void *items[] = {&a, &b, &c};
    
    foreach_item(items, 3, print_with_prefix, "Value: ");
}

When to Use Void Pointers

Void pointers are powerful but add complexity. Use them when you need true generic behavior, not as a default. Choose the simplest approach for your problem.

C
/* Use void* when: */

// 1. Writing truly generic functions
void generic_swap(void *a, void *b, size_t size);
void generic_sort(void *arr, size_t count, size_t size, 
                 int (*cmp)(const void*, const void*));

// 2. Generic data structures
typedef struct {
    void *data;
    struct Node *next;
} GenericNode;

// 3. Interfacing with system APIs
void* pthread_create_wrapper(void *(*start_routine)(void*), void *arg);

// 4. Memory allocation/deallocation
void* custom_malloc(size_t size);
void custom_free(void *ptr);

/* Don't use void* when: */

// 1. Type is known and fixed
// Bad:
void process(void *data) {
    int *arr = (int*)data;
    // Always uses int
}

// Good:
void process_ints(int *arr, size_t count) {
    // Type-safe
}

// 2. Simple type variants
// Bad:
void print(void *data, int type);

// Good: Use union or function overloading pattern
typedef union {
    int i;
    double d;
    char c;
} Value;

void print_value(Value v, int type);

// 3. Performance-critical inner loops
// void* requires casts, type checks - overhead

/* Alternatives to void* */

// 1. Macros for type-generic code
#define SWAP(a, b, T) do { T temp = a; a = b; b = temp; } while(0)

// 2. Code generation
// Generate specific versions at build time

// 3. Tagged unions
typedef enum { INT, DOUBLE, STRING } Type;
typedef struct {
    Type type;
    union {
        int i;
        double d;
        char *s;
    } value;
} Variant;

// 4. C11 _Generic (if available)
#define print_any(x) _Generic((x), \
    int: print_int, \
    double: print_double, \
    char*: print_string \
)(x)

Summary & What's Next

Key Takeaways:

  • ✅ void* is generic pointer, can point to any type
  • ✅ Cannot dereference without casting
  • ✅ Cannot do pointer arithmetic (unknown size)
  • ✅ Used by malloc, qsort, generic functions
  • ✅ Enables generic data structures
  • ✅ Always track type information separately
  • ✅ Check NULL before casting and using
  • ✅ Use only when truly generic behavior needed

What's Next?

Now let's learn about strings in C - character arrays and string manipulation!