Void Pointers in C: Syntax, Working, Examples and Embedded Use Cases

Void Pointers in C: Syntax, Working, Examples and Embedded Use Cases

If you’ve written even a small C program, you’ve already used void pointers in C without noticing. Every call to malloc(), memcpy() or qsort() either returns a void * or accepts one.

Yet this topic trips up beginners and seasoned firmware developers alike. Why does *ptr refuse to compile? Why does the same address print garbage after a cast? Why does a code-review tool flag a cast that “works fine”?

This guide answers all of it with tested code, and adds the embedded systems angle most guides skip.

What you’ll learn

  • What a void pointer is, and how it differs from NULL, wild and dangling pointers
  • Syntax, rules and how the compiler treats a void *
  • Six worked examples: generic swap, qsort(), byte dump, callbacks and more
  • Where void pointers appear in embedded C: RTOS tasks, drivers, buffers
  • Disadvantages, alignment and strict-aliasing traps
  • The “cast to int pointer” debate: bad practice or not?
  • Void pointer in C vs C++, best practices and FAQs

A void pointer in C (void *) is a generic pointer that can hold the address of any data type. It has no type or size information, so you must convert it to a typed pointer (like int *) before dereferencing it. That flexibility is why malloc(), memcpy() and qsort() all rely on it.

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.

TermWhat it describesExample
Void pointerThe type is unknownvoid *p = &x;
NULL pointerThe value is “points to nothing”p = NULL;
Wild pointerNever initialized, holds a random addressint *p; (then used)
Dangling pointerPoints to memory that was freed or went out of scopefree(p); then using p

One pointer can be a void pointer and NULL at the same time. They aren’t alternatives.

registor_now_P

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 pointer
  • ptr: 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:

  1. Store: void *vp = &value; saves the address and forgets the type.
  2. Cast: (uint32_t *)vp says “treat this as a 4-byte unsigned integer.”
  3. 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

FunctionSignature (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 *argPasses 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;
}
45 87 260 512 1023
  • 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(&reg, sizeof reg);
    return 0;
}
78 56 34 12
  • 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

Explore Courses - Learn More

Where Void Pointers Show Up in Embedded Systems

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

  1. Wrong type: float * → void * → int * gives garbage and undefined behavior
  2. Misaligned address: int *p = (int *)(buf + 1); on a uint8_t buf[8] can HardFault on strict-alignment cores
  3. 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
  4. A needless cast on malloc() in C: it adds noise, and on older C89-era compilers it could hide a missing #include
  5. 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.

PointCC++
void * → T *Implicit, no cast neededExplicit cast required (static_cast)
T * → void *ImplicitImplicit
Result of malloc()Don’t cast (idiomatic)Must cast, or avoid malloc and use new or containers
NULLOften ((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 toolsvoid *, 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.

Talk to Academic Advisor

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.

FAQs

No. The compiler doesn’t know the size or type of the data. Cast to a typed pointer first, for example *(int *)vp.

It’s implementation-defined but matches other data pointers on mainstream platforms: 8 bytes on 64-bit PCs, 4 bytes on 32-bit ARM Cortex-M and 2 bytes on 8-bit AVR. Use sizeof(void *) to check.

Not in standard C. GCC allows it as an extension (step size of 1 byte) and warns with -Wpointer-arith. For portable code, cast to uint8_t * and add byte offsets.

No. “Void” describes the pointer’s type, while “NULL” describes its value. A void * can be NULL, and so can any other pointer.

Not portably. ISO C doesn’t guarantee the conversion, and gcc -pedantic warns about it. POSIX requires it for dlsym(). To store a function address, use a function pointer type such as void (*)(void).

Author

Embedded Systems trainer – IIES

Updated On: 21-09-26


10+ years of hands-on experience delivering practical training in Embedded Systems and it's design