The Universal Pointer: A Complete Guide to Void Pointers in C

Void pointers, denoted as void*, are C's most flexible pointer type—they can point to data of any type. This "universal" nature makes them essential for writing generic code, implementing dynamic data structures, and interfacing with system libraries. However, with great power comes great responsibility: void pointers cannot be dereferenced directly or used in pointer arithmetic without casting.

What Is a Void Pointer?

A void pointer is a pointer that has no associated data type. It can hold the address of any data type (int, float, struct, etc.), but you cannot directly access the value it points to without casting it to an appropriate type.

void *ptr;  // ptr can point to any data type

Basic Void Pointer Usage

#include <stdio.h>
int main() {
int i = 42;
float f = 3.14;
char c = 'A';
void *vp;
// Void pointer can point to any type
vp = &i;
printf("Pointing to int: %p\n", vp);
vp = &f;
printf("Pointing to float: %p\n", vp);
vp = &c;
printf("Pointing to char: %p\n", vp);
// To access values, must cast
vp = &i;
printf("Value of i: %d\n", *(int*)vp);
vp = &f;
printf("Value of f: %f\n", *(float*)vp);
return 0;
}

Output (addresses will vary):

Pointing to int: 0x7ffc12345670
Pointing to float: 0x7ffc12345674
Pointing to char: 0x7ffc1234567f
Value of i: 42
Value of f: 3.140000

Void Pointers and Pointer Arithmetic

Standard C does not allow pointer arithmetic on void pointers because the size of the pointed-to type is unknown. However, some compilers (like GCC) support it as an extension, treating it as byte arithmetic.

#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
void *vp = arr;
// Standard C - must cast before arithmetic
printf("First element: %d\n", *(int*)vp);
// Cast to char* for byte-by-byte access
char *cp = (char*)vp;
printf("First byte: %d\n", *cp);
// To access next integer, must cast appropriately
int *ip = (int*)vp;
ip++;
printf("Second element: %d\n", *ip);
// GCC extension (not portable)
#ifdef __GNUC__
vp++;  // GCC treats this as moving by 1 byte
printf("With GCC extension: %d\n", *(int*)vp); // Garbage!
#endif
return 0;
}

Generic Functions with Void Pointers

The most common use of void pointers is to write functions that work with multiple data types.

Example 1: Generic Swap Function

#include <stdio.h>
#include <string.h>
void generic_swap(void *a, void *b, size_t size) {
char temp[size];  // VLA - requires C99
memcpy(temp, a, size);
memcpy(a, b, size);
memcpy(b, temp, size);
}
int main() {
// Swap integers
int x = 10, y = 20;
printf("Before swap: x = %d, y = %d\n", x, y);
generic_swap(&x, &y, sizeof(int));
printf("After swap:  x = %d, y = %d\n\n", x, y);
// Swap doubles
double a = 3.14159, b = 2.71828;
printf("Before swap: a = %f, b = %f\n", a, b);
generic_swap(&a, &b, sizeof(double));
printf("After swap:  a = %f, b = %f\n\n", a, b);
// Swap characters
char c1 = 'A', c2 = 'Z';
printf("Before swap: c1 = %c, c2 = %c\n", c1, c2);
generic_swap(&c1, &c2, sizeof(char));
printf("After swap:  c1 = %c, c2 = %c\n", c1, c2);
return 0;
}

Example 2: Generic Linear Search

#include <stdio.h>
#include <string.h>
void* generic_search(const void *key, const void *base, 
size_t num, size_t size,
int (*compare)(const void*, const void*)) {
const char *bytes = (const char*)base;
for (size_t i = 0; i < num; i++) {
const void *elem = bytes + i * size;
if (compare(key, elem) == 0) {
return (void*)elem;  // Found
}
}
return NULL;  // Not found
}
int compare_int(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
int compare_double(const void *a, const void *b) {
double diff = *(double*)a - *(double*)b;
return (diff > 0) - (diff < 0);
}
int compare_string(const void *a, const void *b) {
return strcmp(*(const char**)a, *(const char**)b);
}
int main() {
// Search in integer array
int ints[] = {5, 2, 8, 1, 9, 3, 7, 4, 6};
int key_int = 7;
size_t int_count = sizeof(ints) / sizeof(ints[0]);
int *found_int = generic_search(&key_int, ints, int_count, 
sizeof(int), compare_int);
if (found_int) {
printf("Found %d at position %ld\n", key_int, found_int - ints);
} else {
printf("%d not found\n", key_int);
}
// Search in double array
double doubles[] = {3.14, 2.71, 1.61, 1.41, 2.23};
double key_double = 1.61;
size_t double_count = sizeof(doubles) / sizeof(doubles[0]);
double *found_double = generic_search(&key_double, doubles, double_count,
sizeof(double), compare_double);
if (found_double) {
printf("Found %f at position %ld\n", key_double, found_double - doubles);
} else {
printf("%f not found\n", key_double);
}
// Search in array of strings
const char *strings[] = {"apple", "banana", "cherry", "date", "elderberry"};
const char *key_string = "cherry";
size_t string_count = sizeof(strings) / sizeof(strings[0]);
const char **found_string = generic_search(&key_string, strings, string_count,
sizeof(char*), compare_string);
if (found_string) {
printf("Found '%s' at position %ld\n", key_string, found_string - strings);
} else {
printf("'%s' not found\n", key_string);
}
return 0;
}

Implementing Generic Data Structures

Example: Generic Dynamic Array

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
void *data;
size_t element_size;
size_t capacity;
size_t size;
} GenericArray;
GenericArray* array_create(size_t element_size, size_t initial_capacity) {
GenericArray *arr = malloc(sizeof(GenericArray));
if (!arr) return NULL;
arr->data = malloc(element_size * initial_capacity);
if (!arr->data) {
free(arr);
return NULL;
}
arr->element_size = element_size;
arr->capacity = initial_capacity;
arr->size = 0;
return arr;
}
void array_destroy(GenericArray *arr) {
if (arr) {
free(arr->data);
free(arr);
}
}
int array_push(GenericArray *arr, const void *element) {
if (!arr || !element) return -1;
if (arr->size >= arr->capacity) {
// Double the capacity
size_t new_capacity = arr->capacity * 2;
void *new_data = realloc(arr->data, arr->element_size * new_capacity);
if (!new_data) return -1;
arr->data = new_data;
arr->capacity = new_capacity;
}
// Copy element to array
char *dest = (char*)arr->data + arr->size * arr->element_size;
memcpy(dest, element, arr->element_size);
arr->size++;
return 0;
}
void* array_get(GenericArray *arr, size_t index) {
if (!arr || index >= arr->size) return NULL;
return (char*)arr->data + index * arr->element_size;
}
size_t array_size(GenericArray *arr) {
return arr ? arr->size : 0;
}
void array_print_int(GenericArray *arr) {
printf("[");
for (size_t i = 0; i < arr->size; i++) {
int *val = array_get(arr, i);
printf("%d", *val);
if (i < arr->size - 1) printf(", ");
}
printf("]\n");
}
void array_print_double(GenericArray *arr) {
printf("[");
for (size_t i = 0; i < arr->size; i++) {
double *val = array_get(arr, i);
printf("%.2f", *val);
if (i < arr->size - 1) printf(", ");
}
printf("]\n");
}
int main() {
// Array of integers
GenericArray *int_arr = array_create(sizeof(int), 5);
int values[] = {10, 20, 30, 40, 50, 60, 70};
for (int i = 0; i < 7; i++) {
array_push(int_arr, &values[i]);
}
printf("Integer array (size=%zu): ", array_size(int_arr));
array_print_int(int_arr);
// Array of doubles
GenericArray *double_arr = array_create(sizeof(double), 3);
double dvals[] = {1.1, 2.2, 3.3, 4.4, 5.5};
for (int i = 0; i < 5; i++) {
array_push(double_arr, &dvals[i]);
}
printf("Double array (size=%zu): ", array_size(double_arr));
array_print_double(double_arr);
array_destroy(int_arr);
array_destroy(double_arr);
return 0;
}

Void Pointers and Function Pointers

Function pointers can also be cast to void*, though this is implementation-dependent:

#include <stdio.h>
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
// Generic function caller using void pointer
int call_function(void *func_ptr, int x, int y) {
// Cast back to appropriate function pointer type
int (*func)(int, int) = (int (*)(int, int))func_ptr;
return func(x, y);
}
int main() {
// Store function pointers as void*
void *funcs[] = {
(void*)add,
(void*)subtract,
(void*)multiply
};
const char *names[] = {"add", "subtract", "multiply"};
int a = 10, b = 5;
for (int i = 0; i < 3; i++) {
int result = call_function(funcs[i], a, b);
printf("%s(%d, %d) = %d\n", names[i], a, b, result);
}
return 0;
}

Void Pointers in System Calls

Many system functions use void pointers for flexibility:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
typedef struct {
int id;
const char *name;
double value;
} ThreadData;
void* thread_function(void *arg) {
ThreadData *data = (ThreadData*)arg;
printf("Thread %d (%s): value = %f\n", 
data->id, data->name, data->value);
return (void*)(intptr_t)(data->id * 100);  // Return value
}
int main() {
pthread_t threads[3];
ThreadData datas[3] = {
{1, "Thread A", 1.23},
{2, "Thread B", 4.56},
{3, "Thread C", 7.89}
};
// Create threads
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, &datas[i]);
}
// Join threads and get return values
for (int i = 0; i < 3; i++) {
void *retval;
pthread_join(threads[i], &retval);
printf("Thread %d returned %ld\n", i, (intptr_t)retval);
}
return 0;
}

qsort and bsearch with Void Pointers

The standard library uses void pointers for generic sorting and searching:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Comparison functions for qsort
int compare_int(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
int compare_double_desc(const void *a, const void *b) {
double diff = *(double*)b - *(double*)a;  // Descending order
return (diff > 0) - (diff < 0);
}
int compare_string(const void *a, const void *b) {
return strcmp(*(const char**)a, *(const char**)b);
}
int compare_struct(const void *a, const void *b) {
const struct Person *pa = a;
const struct Person *pb = b;
return pa->age - pb->age;
}
struct Person {
char name[50];
int age;
};
int main() {
// Sort integers
int ints[] = {5, 2, 8, 1, 9, 3, 7, 4, 6};
size_t int_count = sizeof(ints) / sizeof(ints[0]);
qsort(ints, int_count, sizeof(int), compare_int);
printf("Sorted ints: ");
for (size_t i = 0; i < int_count; i++) {
printf("%d ", ints[i]);
}
printf("\n");
// Sort doubles (descending)
double doubles[] = {3.14, 2.71, 1.61, 1.41, 2.23};
size_t double_count = sizeof(doubles) / sizeof(doubles[0]);
qsort(doubles, double_count, sizeof(double), compare_double_desc);
printf("Sorted doubles (desc): ");
for (size_t i = 0; i < double_count; i++) {
printf("%.2f ", doubles[i]);
}
printf("\n");
// Sort strings
const char *strings[] = {"banana", "apple", "cherry", "date", "elderberry"};
size_t string_count = sizeof(strings) / sizeof(strings[0]);
qsort(strings, string_count, sizeof(char*), compare_string);
printf("Sorted strings: ");
for (size_t i = 0; i < string_count; i++) {
printf("%s ", strings[i]);
}
printf("\n");
// Sort structures
struct Person people[] = {
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
{"Diana", 28}
};
size_t people_count = sizeof(people) / sizeof(people[0]);
qsort(people, people_count, sizeof(struct Person), compare_struct);
printf("Sorted people by age:\n");
for (size_t i = 0; i < people_count; i++) {
printf("  %s: %d\n", people[i].name, people[i].age);
}
return 0;
}

Advanced: Generic Container with Type Information

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef enum {
TYPE_INT,
TYPE_DOUBLE,
TYPE_STRING,
TYPE_CUSTOM
} DataType;
typedef struct {
DataType type;
size_t size;
void (*print)(const void*);
void (*free)(void*);
} TypeInfo;
typedef struct {
void *data;
TypeInfo *type_info;
} GenericValue;
typedef struct {
GenericValue *items;
size_t capacity;
size_t size;
} GenericContainer;
// Print functions for different types
void print_int(const void *data) {
printf("%d", *(int*)data);
}
void print_double(const void *data) {
printf("%f", *(double*)data);
}
void print_string(const void *data) {
printf("\"%s\"", *(char**)data);
}
// Type info instances
TypeInfo int_type = {TYPE_INT, sizeof(int), print_int, NULL};
TypeInfo double_type = {TYPE_DOUBLE, sizeof(double), print_double, NULL};
TypeInfo string_type = {TYPE_STRING, sizeof(char*), print_string, NULL};
GenericContainer* container_create(size_t initial_capacity) {
GenericContainer *c = malloc(sizeof(GenericContainer));
if (!c) return NULL;
c->items = malloc(initial_capacity * sizeof(GenericValue));
if (!c->items) {
free(c);
return NULL;
}
c->capacity = initial_capacity;
c->size = 0;
return c;
}
void container_destroy(GenericContainer *c) {
if (!c) return;
for (size_t i = 0; i < c->size; i++) {
if (c->items[i].type_info->free) {
c->items[i].type_info->free(c->items[i].data);
}
free(c->items[i].data);
}
free(c->items);
free(c);
}
int container_add(GenericContainer *c, const void *value, TypeInfo *type) {
if (!c || !value || !type) return -1;
if (c->size >= c->capacity) {
size_t new_capacity = c->capacity * 2;
GenericValue *new_items = realloc(c->items, 
new_capacity * sizeof(GenericValue));
if (!new_items) return -1;
c->items = new_items;
c->capacity = new_capacity;
}
// Allocate space for the value
void *data_copy = malloc(type->size);
if (!data_copy) return -1;
memcpy(data_copy, value, type->size);
c->items[c->size].data = data_copy;
c->items[c->size].type_info = type;
c->size++;
return 0;
}
void container_print(GenericContainer *c) {
printf("[");
for (size_t i = 0; i < c->size; i++) {
c->items[i].type_info->print(c->items[i].data);
if (i < c->size - 1) printf(", ");
}
printf("]\n");
}
int main() {
GenericContainer *c = container_create(5);
// Add mixed types
int i = 42;
double d = 3.14159;
const char *s = "Hello, World!";
container_add(c, &i, &int_type);
container_add(c, &d, &double_type);
container_add(c, &s, &string_type);
int j = 100;
container_add(c, &j, &int_type);
printf("Container contents: ");
container_print(c);
container_destroy(c);
return 0;
}

Void Pointers and Memory Allocation

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Generic memory allocator with type tagging
typedef struct {
void *ptr;
size_t size;
const char *type_name;
} MemoryBlock;
MemoryBlock* allocate_memory(size_t size, const char *type_name) {
MemoryBlock *block = malloc(sizeof(MemoryBlock));
if (!block) return NULL;
block->ptr = malloc(size);
if (!block->ptr) {
free(block);
return NULL;
}
block->size = size;
block->type_name = type_name;
return block;
}
void free_memory(MemoryBlock *block) {
if (block) {
printf("Freeing %zu bytes of type '%s'\n", 
block->size, block->type_name);
free(block->ptr);
free(block);
}
}
int main() {
// Allocate different types
MemoryBlock *int_block = allocate_memory(sizeof(int), "int");
MemoryBlock *array_block = allocate_memory(10 * sizeof(double), "double array");
MemoryBlock *struct_block = allocate_memory(sizeof(struct {int x; float y;}), "struct");
// Use the memory
if (int_block) {
*(int*)int_block->ptr = 42;
printf("int value: %d\n", *(int*)int_block->ptr);
}
if (array_block) {
double *arr = (double*)array_block->ptr;
for (int i = 0; i < 10; i++) {
arr[i] = i * 1.5;
}
printf("array[5]: %f\n", arr[5]);
}
// Free memory
free_memory(int_block);
free_memory(array_block);
free_memory(struct_block);
return 0;
}

Common Pitfalls and Best Practices

1. Dereferencing Without Cast

void *vp;
int i = 42;
vp = &i;
// WRONG - can't dereference void*
// printf("%d\n", *vp);
// CORRECT
printf("%d\n", *(int*)vp);

2. Pointer Arithmetic Without Cast

int arr[] = {1, 2, 3, 4, 5};
void *vp = arr;
// WRONG - arithmetic on void* is not standard
// vp++;  
// CORRECT - cast before arithmetic
int *ip = (int*)vp;
ip++;
printf("%d\n", *ip);

3. Alignment Issues

#include <stdio.h>
#include <stdlib.h>
struct Data {
char c;
double d;
int i;
};
int main() {
void *vp = malloc(sizeof(struct Data));
// Safe: properly aligned for any type
struct Data *dp = (struct Data*)vp;
dp->d = 3.14159;
// Potentially unsafe: might be misaligned
char *cp = (char*)vp;
double *bad_dp = (double*)(cp + 1);  // May cause alignment issues
free(vp);
return 0;
}

4. Type Information Loss

void process(void *data) {
// PROBLEM: We don't know what type data points to!
// Need to pass type information separately
}
// Better: pass type information
void process_safe(void *data, const char *type) {
if (strcmp(type, "int") == 0) {
printf("Processing int: %d\n", *(int*)data);
} else if (strcmp(type, "double") == 0) {
printf("Processing double: %f\n", *(double*)data);
}
}

5. Const Correctness

const int ci = 42;
void *vp = (void*)&ci;  // Casting away const
// DANGER: modifying through vp can cause undefined behavior
*(int*)vp = 100;  // Undefined behavior!
// Better: preserve const
const void *cvp = &ci;
// *(int*)cvp = 100;  // Compiler error (can't modify const)

Void Pointer Cheat Sheet

OperationSyntaxNotes
Declarationvoid *ptr;Can point to any type
Assignmentptr = &x;No cast needed
Dereference*(type*)ptrMust cast before dereferencing
Arithmetic(char*)ptr + nMust cast before arithmetic
Comparisonptr1 == ptr2Can compare directly
Array access((type*)ptr)[i]Cast to appropriate type
Sizesizeof(void*)Same as other pointers
NULLptr = NULL;Valid for void pointers

When to Use Void Pointers

Good Uses:

  • Generic functions (qsort, bsearch)
  • Generic data structures
  • Callback contexts
  • Opaque handles
  • Memory pools

Bad Uses:

  • When type information is needed
  • When pointer arithmetic is required
  • When you'd lose type safety unnecessarily
  • In performance-critical code with many casts

Best Practices

  1. Always cast before dereferencing - Never dereference a void pointer directly
  2. Cast before arithmetic - Use char* for byte-level operations
  3. Pass type information - Either through function parameters or structs
  4. Document the expected type - Make it clear what type the void pointer points to
  5. Consider alignment - Ensure casts maintain proper alignment
  6. Preserve const qualifiers - Be careful when casting away const
  7. Use typed pointers when possible - Void pointers reduce type safety

Conclusion

Void pointers are C's mechanism for writing generic code that can work with multiple data types. They're essential for:

  • Generic algorithms (sorting, searching)
  • Flexible data structures (linked lists, trees, arrays)
  • System programming (thread arguments, callback contexts)
  • Memory management (allocators, pools)

Key principles to remember:

  • Void pointers can point to any type but cannot be dereferenced directly
  • Always cast to the correct type before dereferencing or arithmetic
  • Pass type information separately when needed
  • Be mindful of alignment and const correctness
  • Use them judiciously—they trade type safety for flexibility

Mastering void pointers enables you to write reusable, type-agnostic code that forms the foundation of many C libraries and frameworks. They're a powerful tool in the C programmer's arsenal, but with that power comes the responsibility to maintain type safety through careful design and documentation.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper