What Is a Void Pointer in C?
A void pointer is a pointer declared as void * that stores a memory address without remembering what type of data lives there. Because the type is missing, it can point to an int, a float, a struct or an array. The catch is that you must supply the type yourself before using the data.
Picture a courier parcel with a delivery address but no label describing what’s inside. You know exactly where it is, but not what it is. The void pointer is the address. The cast is you writing the label.
Key facts
- Also called a generic pointer or typeless pointer
- Stores only an address: no type, no size
- Accepts the address of any object without a cast (in C)
- Cannot be dereferenced until it’s cast to a typed pointer
- Can be
NULL, like any other pointer
Void Pointer vs NULL, Wild and Dangling Pointers
These terms get mixed up constantly because they describe different things.
| Term | What it describes | Example |
|---|
| Void pointer | The type is unknown | void *p = &x; |
| NULL pointer | The value is “points to nothing” | p = NULL; |
| Wild pointer | Never initialized, holds a random address | int *p; (then used) |
| Dangling pointer | Points to memory that was freed or went out of scope | free(p); then using p |
One pointer can be a void pointer and NULL at the same time. They aren’t alternatives.

Syntax of Void Pointer in C
c
void *ptr; // declaration
void *ptr = NULL; // better: initialize at the point of declaration
void: no type information*: this is a pointerptr: the variable name
Rules that never change
- Any object pointer converts to
void * without a cast in C - To read or write the data, cast first:
*(int *)ptr T * → void * → T * gives you back the original pointer- For read-only data, declare it as
const void * - Function pointers can’t be stored in a
void * portably (see the FAQ)
What happens if you skip the cast?
c
int x = 10;
void *vp = &x;
int y = *vp; // compile error
int z = *(int *)vp; // cast first, then dereference
GCC replies with dereferencing 'void *' pointer, followed by void value not ignored as it ought to be. The compiler has no idea how many bytes to read, so it refuses.
How big is a void pointer? On mainstream platforms it’s the same size as other data pointers: 8 bytes on a 64-bit PC, 4 bytes on 32-bit ARM Cortex-M (STM32) and 2 bytes on 8-bit AVR. Check with sizeof(void *).
How Does a Void Pointer Work in C?
A void pointer stores only an address. The cast supplies the missing information: how many bytes to read and how to interpret them. It happens in three steps:
- Store:
void *vp = &value; saves the address and forgets the type. - Cast:
(uint32_t *)vp says “treat this as a 4-byte unsigned integer.” - Dereference:
*(uint32_t *)vp now generates the correct load instruction.
Same address, different “lenses”:
c
uint32_t reg = 0x12345678;
void *vp = ® // vp holds only the address of reg
uint32_t whole = *(uint32_t *)vp; // read 4 bytes -> 0x12345678
uint8_t first = *(uint8_t *)vp; // read 1 byte -> 0x78 (little-endian)
The pointer never changed, only your interpretation did. On little-endian CPUs (ARM Cortex-M, x86) the first byte is 0x78. On a big-endian CPU it would be 0x12.
Why Can’t You Do Arithmetic on a Void Pointer?
p + 1 normally moves the pointer by sizeof(*p) bytes. For void there is no size, so standard C doesn’t define it. GCC allows it as an extension (treating the size as 1) but warns under -Wpointer-arith. The portable fix is to cast to a byte pointer:
c
void *vp = buffer;
vp = (uint8_t *)vp + 4; // move 4 bytes ahead, portable
// vp++; // not standard C
Why Use a Void Pointer in C?
Use a void pointer when one function or data structure has to work with many data types without duplicating code. Its main jobs:
- Generic functions: one
swap_any() instead of swap_int(), swap_float() and swap_sensor() - Dynamic memory:
malloc(), calloc() and realloc() return void *, so one allocator serves every type - Callbacks with context: hand “user data” to a handler without the driver knowing your struct
- Generic containers: linked lists, queues, stacks and ring buffers that store any element type
- Byte-level work:
memcpy(), memset(), memcmp(), DMA buffers, protocol parsing - Hardware abstraction: driver and HAL layers pass opaque handles and parameters
Void Pointers You Already Use
| Function | Signature (simplified) | Why void *? |
|---|
malloc() | void *malloc(size_t n) | One allocator for every type |
memcpy() | void *memcpy(void *dst, const void *src, size_t n) | Copies raw bytes of any object |
qsort() | void qsort(void *base, size_t n, size_t size, cmp) | Sorts arrays of any type |
pthread_create() | ..., void *(*start)(void *), void *arg | Passes any argument to a thread |
xTaskCreate() (FreeRTOS) | ..., void *pvParameters, ... | Passes any parameter into a task |
xQueueSend() (FreeRTOS) | ..., const void *pvItemToQueue, ... | Queues items of any type |
Examples of Void Pointer in C
These examples cover generic functions, sorting, raw-byte handling and callbacks. Each one compiles without warnings, and the output shown is what we got.
Example 1: One Void Pointer, Three Data Types
c
#include
int main(void)
{
int rpm = 1500;
float temp = 36.5f;
char mode = 'A';
void *vp;
vp = &rpm;
printf("int -> %d\n", *(int *)vp);
vp = &temp;
printf("float -> %.1f\n", *(float *)vp);
vp = &mode;
printf("char -> %c\n", *(char *)vp);
return 0;
}
int -> 1500
float -> 36.5
char -> A
vp is reassigned three times, and no cast is needed to store an address- A cast is needed every time we read the value
Example 2: Generic Swap Function
c
#include
#include
void swap_any(void *a, void *b, size_t size)
{
unsigned char *pa = a;
unsigned char *pb = b;
while (size--) {
unsigned char tmp = *pa;
*pa++ = *pb;
*pb++ = tmp;
}
}
int main(void)
{
int x = 5, y = 9;
double p = 1.5, q = 2.5;
swap_any(&x, &y, sizeof x);
swap_any(&p, &q, sizeof p);
printf("x = %d, y = %d\n", x, y);
printf("p = %.1f, q = %.1f\n", p, q);
return 0;
}
x = 9, y = 5
p = 2.5, q = 1.5
- The function swaps raw bytes, so it doesn’t care whether it’s an
int or a double size is the price of generality: the function can’t work it out, so you pass it in- In C,
unsigned char *pa = a; needs no cast
Example 3: Sorting Any Array with qsort()
c
#include
#include
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 adc[] = { 512, 87, 1023, 260, 45 };
size_t n = sizeof adc / sizeof adc[0];
qsort(adc, n, sizeof adc[0], cmp_int);
for (size_t i = 0; i < n; i++) {
printf("%d ", adc[i]);
}
printf("\n");
return 0;
}
qsort() hands your comparator two const void *, and you cast them back to int- Return
(x > y) - (x < y), not x - y. Subtraction can overflow with large values
Example 4: Dump Any Variable as Bytes
c
#include
#include
#include
static void dump_bytes(const void *data, size_t len)
{
const unsigned char *p = data;
for (size_t i = 0; i < len; i++) {
printf("%02X ", p[i]);
}
printf("\n");
}
int main(void)
{
uint32_t reg = 0x12345678;
dump_bytes(®, sizeof reg);
return 0;
}
- Works on any object: a register value, a struct, a packet buffer
- The output reveals byte order: the least significant byte comes first on little-endian CPUs
const void * promises the function won’t modify your data
Example 5: Callback with Context
c
#include
typedef void (*event_cb_t)(void *ctx);
typedef struct {
const char *name;
int count;
} button_t;
static void on_press(void *ctx)
{
button_t *btn = ctx; // recover the real type
btn->count++;
printf("%s pressed %d time(s)\n", btn->name, btn->count);
}
static void fire_event(event_cb_t cb, void *ctx)
{
cb(ctx); // the "driver" never learns what ctx is
}
int main(void)
{
button_t user_btn = { "USER_BTN", 0 };
fire_event(on_press, &user_btn);
fire_event(on_press, &user_btn);
return 0;
}
USER_BTN pressed 1 time(s)
USER_BTN pressed 2 time(s)
- The event code stays reusable because it never depends on
button_t - The callback recovers the real type on its first line
- Keep the context object alive for as long as the callback can fire (more on this below)
Example 6: Type-Tagged Data
c
#include
typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } data_type_t;
typedef struct {
data_type_t type;
void *data;
} variant_t;
static void print_variant(const variant_t *v)
{
switch (v->type) {
case TYPE_INT: printf("int : %d\n", *(int *)v->data); break;
case TYPE_FLOAT: printf("float : %.2f\n", *(float *)v->data); break;
case TYPE_STRING: printf("string : %s\n", (const char *)v->data); break;
}
}
int main(void)
{
int temp = 27;
float volt = 3.30f;
char board[] = "STM32";
variant_t items[] = {
{ TYPE_INT, &temp },
{ TYPE_FLOAT, &volt },
{ TYPE_STRING, board }
};
for (size_t i = 0; i < sizeof items / sizeof items[0]; i++) {
print_variant(&items[i]);
}
return 0;
}
int : 27
float : 3.30
string : STM32
- A bare
void * can’t say what it points to, so the enum does - Pairing pointer + tag is how message queues and protocol handlers stay safe

In embedded C, void pointers are the glue between reusable low-level code and your application-specific data.
- RTOS tasks:
xTaskCreate() passes a void * into each task, and you cast it back on the first line - Queues and mailboxes:
xQueueSend() takes a const void * and copies the item using the size fixed at queue creation, so one API serves sensor readings, CAN frames or commands - Driver callbacks: a
void (*cb)(void *ctx) handler lets an interrupt-driven driver call back into your object without knowing its type - Ring buffers and memory pools: allocators hand out raw blocks as
void *, and the caller casts to its own struct - DMA and protocol buffers: the same buffer is seen as bytes when transmitting and as a struct when parsing
- Bootloaders and flash drivers: copy pages of raw data with a
const void * and a length
A simplified FreeRTOS-style snippet (needs the FreeRTOS headers and your own GPIO function):
c
static uint8_t green_led_pin = 5; // static: outlives the function that creates the task
void vBlinkTask(void *pvParameters)
{
uint8_t pin = *(uint8_t *)pvParameters; // recover the real type
for (;;) {
gpio_toggle(pin); // your HAL call
vTaskDelay(pdMS_TO_TICKS(500));
}
}
/* in main(): */
xTaskCreate(vBlinkTask, "blink", 128, &green_led_pin, 1, NULL);
Field note: One of the most common void pointer bugs in firmware isn’t syntax, it’s lifetime. Pass &local_variable as a callback context, let the function return, and the void * still holds the address of a stack slot you no longer own. No compiler will warn you. Give context objects static storage (or a memory pool) instead.
What Are the Disadvantages of Void Pointer in C Language?
The main disadvantage of a void pointer is that it removes type safety: the compiler can no longer stop you from reading data as the wrong type.
- No type safety: a wrong cast compiles silently
- No direct dereferencing: extra casts everywhere hurt readability
- No standard pointer arithmetic: you must cast to a byte pointer first
- Bookkeeping burden: you have to carry the size or type separately
- Alignment risk: on cores like ARM Cortex-M0/M0+, an unaligned access triggers a HardFault
- Strict-aliasing undefined behavior: reading an object through the wrong type lets the optimizer break your code
- Harder debugging: the debugger shows a bare address, and you must cast in the watch window
- Lifetime blindness: nothing in
void * says who owns the memory or how long it stays valid - Function pointers: ISO C doesn’t guarantee a
void * can hold one - Tool friction: MISRA and CERT checkers flag conversions from
void *
The Classic Bug: Compiles, Runs, Prints Nonsense
c
float temperature = 36.5f;
void *vp = &temperature;
int wrong = *(int *)vp; // no warning, even with -Wall -Wextra -pedantic
printf("%d\n", wrong); // prints 1108475904, the raw bits of 36.5f
That number is just the IEEE-754 bit pattern of 36.5 (0x42120000) shown as an integer. It’s also undefined behavior, because reading a float through an int * breaks the strict-aliasing rule. When we compiled the same code at -O2, GCC 13 even warned that temperature was used uninitialized. The optimizer assumed an int read can’t touch a float.
The safe way to reinterpret bits is memcpy():
c
int bits;
memcpy(&bits, &temperature, sizeof bits); // well-defined; prints 0x42120000 at -O2 too
Why Is It Considered Bad Practice to Cast a Void Pointer to an Int Pointer in C?
Casting a void pointer to int * isn’t bad practice by itself. It’s the normal way to use a void *. It becomes a problem when the memory doesn’t really hold an int, when the address isn’t suitably aligned, or when the cast hides a mistake.
When the Cast Is Perfectly Fine
- The
void * was created from an int * (the C standard guarantees the round trip) - The buffer came from
malloc() and was sized for ints - You’re reading raw bytes through
unsigned char * (always allowed)
When It Becomes Bad Practice
- Wrong type:
float * → void * → int * gives garbage and undefined behavior - Misaligned address:
int *p = (int *)(buf + 1); on a uint8_t buf[8] can HardFault on strict-alignment cores - Casting away
const: turning a const void * into int * and writing through it is undefined behavior. Writing to const data placed in flash can fault or silently fail - A needless cast on
malloc() in C: it adds noise, and on older C89-era compilers it could hide a missing #include - Confusing “pointer to int” with “pointer as int”:
int addr = (int)vp; truncates on 64-bit systems. Use uintptr_t
c
/* The type follows the variable: no cast, no repeated type name */
int *samples = malloc(n * sizeof *samples);
/* Works, but noisy and easy to get out of sync */
int *samples = (int *)malloc(n * sizeof(int));
What the coding standards say
- MISRA C:2012 Rule 11.5 is an advisory rule against converting a pointer to void into a pointer to an object, because the result can be misaligned, which is undefined behavior. mathworks
- CERT’s EXP36-C makes a related point: don’t convert pointers into more strictly aligned pointer types. iar
- Real projects handle unavoidable cases with documented deviations. FreeRTOS’s MISRA notes justify their casts by pointing out that blocks returned by
pvPortMalloc() are guaranteed to meet the port’s alignment requirement.
Difference Between a Void Pointer in C and C++
The biggest difference is conversion strictness: C converts a void * to any object pointer implicitly, while C++ demands an explicit cast.
| Point | C | C++ |
|---|
void * → T * | Implicit, no cast needed | Explicit cast required (static_cast) |
T * → void * | Implicit | Implicit |
Result of malloc() | Don’t cast (idiomatic) | Must cast, or avoid malloc and use new or containers |
NULL | Often ((void *)0) | Can’t be (void *)0; use nullptr (C++11) |
Arithmetic on void * | Not standard (GCC extension, size 1) | Not standard (same GCC extension, with a warning) |
| Generic programming tools | void *, macros, _Generic (C11) | Templates, overloads, std::variant, std::any |
Function pointer ↔ void * | Not guaranteed by ISO C (POSIX requires it) | Conditionally supported |
c
/* C: compiles as-is */
int *p = malloc(sizeof *p);
cpp
// C++: the same line fails with "invalid conversion from 'void*' to 'int*'"
int *p = static_cast(std::malloc(sizeof(int))); // explicit cast required
In C++ you rarely need void * at all. Templates, overloading, virtual functions and smart pointers cover most cases with full type checking.
Best Practices for Void Pointers in C
- Reach for typed pointers first. Use
void * only when you truly need generic behavior - Ship a size or type tag with every
void *. A size_t len or an enum saves you from guessing - Use
const void * for inputs the function must not modify - Convert once, at the top of the function.
button_t *btn = ctx;, then work with the typed pointer - Do byte math on
uint8_t *, never on a bare void * - Check for
NULL before casting or using - Document ownership and lifetime: who allocates, who frees, how long it stays valid
- Use
uintptr_t, never int, when a pointer must live in an integer - Reinterpret bits with
memcpy(), not with a pointer cast - Turn on the warnings:
-Wall -Wextra -Wcast-qual -Wcast-align -Wpointer-arith, and run a MISRA/CERT static-analysis pass on firmware
Safer Alternative 1: Opaque Handles
c
/* uart.h: public header */
typedef struct uart_dev uart_dev_t; // fields hidden inside uart.c
uart_dev_t *uart_open(int port);
void uart_send(uart_dev_t *dev, const void *data, size_t len);
The handle is a real struct pointer, so the compiler catches mix-ups. The payload stays a const void *, which is where generic data belongs.
Safer Alternative 2: _Generic (C11)
c
#define print_val(x) _Generic((x), \
int: print_int, \
float: print_float, \
const char *: print_str, \
char *: print_str)(x)
print_val(42); // calls print_int
print_val(3.3f); // calls print_float
print_val("hello"); // calls print_str
The compiler picks the function from the argument type at compile time, so you get generic-looking code with full type checking. We compiled and ran this with GCC 13 in C11 mode.

Key Takeaways
- A void pointer is an address without a type: cast before you dereference
- It powers generic functions, callbacks, containers and memory APIs
- The costs are no type safety, alignment and aliasing risks, and harder debugging
- Casting to
int * is fine when the data is an int, and wrong when type, alignment or const don’t match - C converts implicitly, while C++ demands a cast and offers better tools
- In embedded code, pass sizes, document lifetimes and keep casts in a few reviewed spots
Master void pointers in C and pointers as a whole start to make sense. Next, try rewriting Example 3 to sort an array of structs by one field. That’s the moment void * clicks.