Void Pointer

Hi everyone ✋

In the previous post we stacked pointers on top of pointers and conquered that dizzying double asterisk ** 😵‍💫. In this post we are gonna learn about ─ the mysterious, “typeless” void pointer 😉

If you haven’t read the earlier posts in this series, please go through them first ─ especially the very first post and the ones on arrays and dynamic memory, because today we lean on all of them.

So here’s the strange promise of today’s topic ─ a single pointer that can point at

  • an int
  • a char
  • a double
  • a whole struct
  • anything 🤯

Sounds too good to be true, right?

Well… it comes with a catch. And once we understand the catch, we’ll spend the second half of this post seeing where void pointers actually show up in real, shipping code ─ from Unix thread APIs to callbacks, generic data structures, sorting, and even the guts of modern C++. This one is a long one, so grab a coffee ☕.

Let’s take a deep dive 🦴

A quick refresher ─ why normal pointers are so “picky”

Before we can appreciate the void pointer, we have to remember one thing about ordinary pointers ─ they are strongly typed. An int* can only point at an int, a char* only at a char, and so on. We can’t mix them without complaining from the compiler.

But why is a pointer so fussy about type? It’s not just to annoy us 😅. The type is doing a real job.

Think about what happens when we dereference. A pointer stores an address ─ really just the location of the first byte. But how many bytes should it read from there? And how should it interpret them? That’s exactly what the type decides 🤔

  • Dereference a char* → read 1 byte and treat it as a character.
  • Dereference an int* → read 4 bytes and treat them as an integer.
  • Dereference a double* → read 8 bytes and treat them as a floating-point number.

So the type isn’t decoration ─ it’s the instruction manual that tells the pointer how many bytes to grab and how to read them. Take the type away, and the pointer knows where the data is but has no idea what it is. Hold that thought 🧠

The big idea ─ a pointer with no type

A void pointer is a pointer with its type deliberately left blank. We declare it with the keyword void.

void *ptr;

That’s it ─ void *ptr is a pointer that can store the address of any type of variable. It’s often called a generic pointer or a typeless pointer, and both names fit perfectly. Because it carries no type, the usual “an int* only points at an int” rule simply doesn’t apply to it 🥳

#include <iostream>
using namespace std;

int main()
{
    int    a = 15;
    char   c = 'x';
    double d = 3.14;

    void *ptr;      // one pointer to rule them all

    ptr = &a;       // happily points at an int
    ptr = &c;       // now points at a char
    ptr = &d;       // now points at a double

    cout << "A single void pointer held all three addresses!" << endl;

    return 0;
}

No casts, no complaints ─ the same ptr cheerfully accepted the address of an int, a char, and a double, one after another 🥳. Try that with an int* and the compiler would scold us instantly.

My little reference ─ the unlabeled box on a warehouse shelf 📦

This is the mental picture I want us to keep for the whole post.

Imagine a giant warehouse full of shelves. Every ordinary pointer is like a labeled box ─ the label says “Books” or “Mugs” or “Laptops.” The label tells the worker not just where the box is, but what’s inside and roughly how much of it there is.

A void pointer is a box sitting on the exact same shelf ─ except its label has been peeled off 🏷️. The worker still knows the precise shelf location (the address is perfectly fine!). They can pick the box up, carry it around, hand it to a colleague, note down where it lives ─ no problem at all.

But they cannot open it and use what’s inside, because they don’t know what it is or how much to take out. Is it one small item? Four? Eight? Until someone slaps a fresh label on the box saying “this is a box of ints,” its contents stay a mystery 🤔

That act of slapping a label back on ─ “treat this as an int” ─ is exactly what we’ll do in a moment with a cast. Keep this picture close; everything about void pointers falls out of it.

The catch ─ we cannot dereference a void pointer directly 😱

Here’s where the “too good to be true” bill arrives. A void pointer can store any address, but it cannot be dereferenced on its own.

int a = 15;
void *ptr = &a;

cout << *ptr << endl;   // ERROR ─ won't compile! 😱

The compiler flatly refuses. And by now we already know why, because we set it up in the refresher ─ dereferencing means “read the right number of bytes and interpret them.” But ptr has no type, so it has no idea whether to grab 1 byte, 4 bytes, or 8, nor how to make sense of them.

It’s our unlabeled box exactly 📦 ─ the worker knows the shelf, but with no label they can’t open it and hand us the contents. There’s simply not enough information.

The fix ─ label the box before we open it (type casting) 🥳

The solution is beautiful and it’s the heart of this whole post ─ before we dereference, we cast the void pointer back to a real pointer type. That cast is us slapping a label on the box: “open this as an int.”

#include <iostream>
using namespace std;

int main()
{
    int a = 15;

    void *ptr = &a;               // store the address, type blank

    // cast back to int* first, THEN dereference
    cout << *( (int *)ptr ) << endl;

    return 0;
}

The output will be ─

15

🥳🥳 Let’s read that key expression slowly, from the inside out ─

  • (int *)ptr ─ we tell the compiler “take this typeless address and treat it as the address of an int.” This is the label going back on the box.
  • *( (int *)ptr ) ─ now that it’s an int*, dereferencing is legal again ─ read 4 bytes, interpret as an int, and out comes 15.

So a void pointer is a two-step dance ─ store freely, but cast before we read. The moment we give it a type, it behaves like any pointer we already know 🤔

A small but important note for C++ ─ storing into a void pointer needs no cast (void *ptr = &a; just works, because going from a specific type to “no type” loses nothing). But going the other way ─ from void* back to a specific int*always needs an explicit cast, because we’re adding information the pointer didn’t have. The box can lose its label for free, but putting a label back on is a deliberate act 😉.

This is actually a difference between C and C++.

Old C let us assign a void* to an int* without a cast ☠️, let’s see the C way
#include <stdio.h>
#include <stdlib.h>

int main() {
    int value = 100;

    // 1. Specific to void* (Implicit - allowed in both C and C++)
    void *void_ptr = &value; 

    // 2. void* back to Specific (Implicit - ALLOWED in C, but fails in C++)
    int *int_ptr = void_ptr; 

    // 3. Common C allocation (Implicit void* from malloc)
    int *dynamic_array = malloc(5 * sizeof(int)); 

    printf("Value via pointer: %d\n", *int_ptr);
    
    free(dynamic_array);
    return 0;
}
But C++ tightened the rule to keep us safe. 😇
#include <iostream>
#include <cstdlib>
using namespace std;

int main() {
    int value = 100;

    // 1. Specific to void* (Implicit - still works fine)
    void *void_ptr = &value; 

    // 2. void* back to Specific (Requires explicit static_cast)
    // int *int_ptr = void_ptr; // ERROR: Compilation fails here in C++
    int *int_ptr = static_cast<int*>(void_ptr); 

    // 3. C-style allocation in C++ (Requires explicit cast)
    // int *dynamic_array = malloc(5 * sizeof(int)); // ERROR: Compilation fails
    int *dynamic_array = static_cast<int*>(std::malloc(5 * sizeof(int))); 

    cout << "Value via pointer: " << *int_ptr << std::endl;

    free(dynamic_array);
    return 0;
}

Seeing why the type matters ─ same address, different labels 🔬

This is my favourite part, because it makes the whole “type decides how many bytes” idea visible with our own eyes.

Let’s store the number 2049 and point a void pointer at it. Then we’ll open that same box twice ─ once labelled as int, once labelled as char ─ and watch how the answer changes depending on the label we choose.

First, why 2049? Because its bytes are easy to spot. In memory, 2049 looks like this ─

        fourth    third    second   first
2049 =  00000000 00000000 00001000 00000001

That very first (lowest) byte is 00000001, which is just 1. Keep an eye on it 👀

#include <iostream>
using namespace std;

int main()
{
    int a = 2049;
    void *ptr = &a;

    // open the box as an int ─ read all 4 bytes
    cout << "as int  : " << *( (int *)ptr )  << endl;

    // open the SAME box as a char ─ read only the first 1 byte
    cout << "as char : " << (int)*( (char *)ptr ) << endl;

    return 0;
}

The output will be ─

as int  : 2049
as char : 1

🤯 Whoa ─ the same address gave two different values! Nothing about the memory changed; only the label changed.

  • When we cast to int*, the pointer read all four bytes and reconstructed the full number 2049.
  • When we cast to char*, it read only the first single byte ─ which was 00000001 ─ so we got 1.

This is the entire reason ordinary pointers are strongly typed, seen live 🥳. The type is what tells a pointer how much to read and how to interpret it. A void pointer throws that information away on purpose, so we have to supply it back with a cast ─ and if we supply the wrong label, we get a “correct-looking but wrong” value, exactly like reading only one byte of a four-byte number.

Pointer arithmetic on a void pointer ⚠️

Remember pointer arithmetic from the arrays post?

When we wrote ptr + 1 on an int*, it jumped forward by sizeof(int) bytes ─ not one byte, but one whole element. The pointer knew the step size because it knew its type.

So now ask yourself ─ what should ptr + 1 do on a void pointer? Jump by how much? 🤔 One byte? Four? Eight? The pointer has no type, so it has no step size, and standard C++ therefore forbids arithmetic on a void pointer.

void *ptr = &a;
ptr + 1;          // ERROR in standard C++ ─ no known element size! 😱

The fix is the same as always ─ cast first, so the pointer gains a type (and therefore a step size), then do the arithmetic.

#include <iostream>
using namespace std;

int main()
{
    int a = 2049;
    void *ptr = &a;

    // give it a type, THEN step ─ now +1 means "one int forward"
    int *iptr = (int *)ptr;

    cout << "first  slot address : " << iptr       << endl;
    cout << "second slot address : " << (iptr + 1) << endl;   // + 4 bytes

    return 0;
}

Once labelled as int*, the + 1 knows it means “one int (4 bytes) forward,” just like in the arrays post. Without a label, the box has no idea how big a “step” even is 📦


Where void pointers really earn their keep 🛠️

Alright ─ so far the void pointer looks like more trouble, not less. A pointer we can’t even dereference without extra ceremony? Why on earth does it exist? 🤔

Here’s the big reveal ─ the void pointer’s whole reason to live is generic code. It’s the tool we reach for when we want to write one thing that works with every type, without knowing the type in advance. And it turns out that need shows up everywhere in real software ─ far more than most beginners realise.

Let’s walk through the real problems, one by one, from the everyday standard library all the way to Unix systems programming. This is the part I really want us to take away from this post 💪

Real problem 1 ─ generic memory: the famous void* return

We’ve actually used a void pointer already, maybe without realising it! Back in the dynamic memory post we met C’s classic memory allocator, malloc. How can one function hand back memory for ints, doubles, structs ─ anything at all?

The secret is that malloc returns a void* 🥳. It has no idea what we’re going to store; it just reserves the raw bytes and hands us back the address with the label peeled off. It’s our job to slap the right label on it ─

#include <cstdlib>
#include <iostream>
using namespace std;

int main()
{
    // malloc returns void* ─ raw, typeless memory
    void *raw = malloc(sizeof(int));

    // WE decide the label ─ "treat this as an int"
    int *num = (int *)raw;

    *num = 42;
    cout << *num << endl;

    free(raw);
    return 0;
}

The output will be ─

42

🥳 See the pattern?

malloc speaks in void pointers because it must serve every type equally. The typelessness that felt like a weakness a moment ago is exactly the strength that lets one function allocate memory for the entire language. (In C++ we normally prefer new/delete from the dynamic memory post, but under the hood this same void-pointer idea is everywhere.)

Real problem 2 ─ one sort function for every type: qsort

Here’s a problem that void pointers solve beautifully. Suppose we’re writing a sorting function. We’d have to write one for int arrays, another for double arrays, another for structs… that’s exhausting 😩. The C standard library refuses to do that. It ships one qsort that sorts an array of anything.

How?

The array is passed as a void* (just “some bytes at some address”), we tell it the element size, and we hand it a little comparator function. That comparator receives two const void* ─ and we relabel them 🥳

#include <iostream>
#include <cstdlib>
using namespace std;

// qsort hands our comparator two const void* ─ we relabel them as int*
int compareInts(const void *a, const void *b)
{
    int x = *(const int *)a;
    int y = *(const int *)b;
    return x - y;             // negative, zero, or positive
}

int main()
{
    int arr[] = { 5, 2, 9, 1, 7 };
    int n = 5;

    // base=arr, count=n, size of each element, comparator
    qsort(arr, n, sizeof(int), compareInts);

    for (int i = 0; i < n; i++) cout << arr[i] << " ";
    cout << endl;

    return 0;
}

The output will be ─

1 2 5 7 9

🥳🥳 The magic is that qsort never needs to know it’s sorting ints. It just shuffles raw bytes around, using the element size to know how big each “box” is, and asks our comparator whenever it needs to compare two of them. Swap compareInts for a comparator on struct Student and the same qsort sorts students by grade. One function, infinite types ─ that’s the void pointer’s superpower on full display.

Real problem 3 ─ writing our own “works on anything” function

Let’s build our own generic function to feel the power directly. Suppose we want one printValue function that can print an int or a double, chosen at run time. We pass the address as a void*, plus a little tag telling it which label to use ─

#include <iostream>
using namespace std;

void printValue(void *data, char type)
{
    if (type == 'i')
        cout << "int    : " << *( (int *)data )    << endl;
    else if (type == 'd')
        cout << "double : " << *( (double *)data ) << endl;
}

int main()
{
    int    a = 15;
    double b = 3.14;

    printValue(&a, 'i');   // same function...
    printValue(&b, 'd');   // ...two different types!

    return 0;
}

The output will be ─

int    : 15
double : 3.14

🥳 One function, two completely different types, all thanks to a void* parameter that accepted either address. Inside, the little type tag told us which label to put on the box before opening it.

Real problem 4 ─ Unix / systems programming: passing data into a thread 🧵

Now for the one I’ve reached for many times in real life ─ Unix systems programming. This is where void pointers stop being a curiosity and become a daily tool.

Take POSIX threads (pthreads). When we start a thread, we give it a function to run. But that function must have exactly this signature, fixed by the standard ─

void *function(void *arg);

Look at it ─ it takes a void* and returns a void* 🤯. Why? Because the pthreads library was written once, long ago, and it has to be able to start a thread running our function with our data ─ whatever type that data happens to be. The library authors had no idea what we’d want to pass, so they used the only pointer that fits everything ─ the void pointer.

So the real-world pattern is ─ bundle whatever data we want into a struct, pass its address as a void*, and inside the thread, relabel the box back to our struct 🥳

#include <iostream>
#include <pthread.h>
using namespace std;

struct TaskInfo {
    int    id;
    string name;
};

// must match pthread's required signature: void* f(void*)
void *worker(void *arg)
{
    // put the label back on ─ WE know we passed a TaskInfo*
    TaskInfo *task = (TaskInfo *)arg;

    cout << "Worker " << task->id
         << " (" << task->name << ") is running" << endl;

    return nullptr;
}

int main()
{
    pthread_t thread;
    TaskInfo  task { 1, "downloader" };

    // hand the address of our struct across as a void*
    pthread_create(&thread, nullptr, worker, &task);
    pthread_join(thread, nullptr);

    return 0;
}

Compile it with the pthread flag (g++ file.cpp -pthread) and the output will be ─

Worker 1 (downloader) is running

🥳🥳 This is the void pointer doing exactly what it was born to do ─ letting a general-purpose library (that knew nothing about my TaskInfo) carry my custom data into a thread and back. Every threaded C/C++ program on Unix uses this pattern.

A lovely tie-in to the previous post ─ getting a value back with void** 🤔

Remember the pointers to pointers post, where a function that needs to change our pointer takes a void**?
Pthreads uses that too! A thread can return a result as a void*, and pthread_join‘s second argument is a void** ─ a pointer to our pointer ─ so it can fill it in for us.

void *worker(void *arg)
{
    int *result = new int(99);
    return result;              // hand a value back as void*
}

// ... in main ...
void *ret;
pthread_join(thread, &ret);     // &ret is void** ─ exactly last post's lesson!

int *value = (int *)ret;        // relabel the returned box
cout << *value << endl;         // 99
delete value;

See how the whole series clicks together? 🥳

The thread returns a void*, and to let pthread_join write into our ret pointer, we pass &ret ─ a void**. That’s the “pass a pointer to our pointer so a function can update it” idea from last post, meeting the “typeless pointer” idea from this one.

And it doesn’t stop at threads

Once we start looking, Unix is full of void pointers, all for the same reason ─ the kernel and its libraries must serve every program and every data type ─

  • read() and write() ─ these syscalls take a void *buf, because a buffer is just bytes; it could be text, an image, a struct, anything.
  • mmap() ─ maps a file or memory region and returns a void* to the start of it. We decide what type lives there.
  • dlopen() / dlsym() ─ for loading plugins/shared libraries at run time, dlsym hands us back a void* to a symbol we then cast to the right function or variable type.
  • Signal handlers ─ advanced signal handling (sigaction with SA_SIGINFO) passes a void *context describing the interrupted state.

Every one of these is the same story ─ a general-purpose interface that can’t possibly know our type, so it speaks in the one pointer that fits everything 🤔

Real problem 5 ─ the callback “context pointer” pattern 🎯

This one is everywhere in real applications, and once we see it we’ll spot it in every library we touch.

Imagine we register a callback ─ “when this button is clicked, call my function.” But your function often needs some contextwhich counter to increment, which object to update. The library can’t know what context we need. So the classic solution is a void *userdata slot ─ we stash any data we like, and the library faithfully hands it back to our callback untouched 🥳

#include <iostream>
using namespace std;

// a tiny "widget" that stores a callback plus opaque user data
struct Button {
    void (*onClick)(void *userdata);
    void *userdata;               // carry ANY context we want
};

struct Counter { int clicks; };

void handleClick(void *userdata)
{
    Counter *c = (Counter *)userdata;   // relabel the box
    c->clicks++;
    cout << "Clicked! total = " << c->clicks << endl;
}

int main()
{
    Counter myCounter { 0 };

    Button btn;
    btn.onClick  = handleClick;
    btn.userdata = &myCounter;    // stash our context as void*

    // simulate three clicks
    btn.onClick(btn.userdata);
    btn.onClick(btn.userdata);
    btn.onClick(btn.userdata);

    return 0;
}

The output will be ─

Clicked! total = 1
Clicked! total = 2
Clicked! total = 3

🥳🥳 This exact void* userdata pattern powers a huge amount of real software ─ GUI toolkits like GTK, networking libraries like libcurl (CURLOPT_WRITEDATA), game frameworks like SDL, event loops, timers, and countless C APIs. They all say: “give me a function and a void*; I’ll call your function and hand your void* right back.”

There’s a very common C++ twist here too ─ when a C library only gives us a void* userdata, we can pass our object’s this pointer through it! Inside a plain callback function, we cast the void* back to our class type and call a real method. That’s the standard bridge between old C callback APIs and modern C++ objects 😉

Real problem 6 ─ a generic data structure for any type 📚

Here’s a problem that void pointers solve elegantly in C. Suppose we want a stack. Should we write IntStack, DoubleStack, CharStack, StudentStack…? That’s madness. Instead, we should write one stack that stores void*, and let it hold pointers to anything 🥳

#include <iostream>
using namespace std;

// a stack that can hold pointers to ANY type
struct Stack {
    void *items[100];
    int   top = 0;
};

void  push(Stack &s, void *item) { s.items[s.top++] = item; }
void *pop (Stack &s)             { return s.items[--s.top]; }

int main()
{
    Stack s;

    int    a = 42;
    double b = 3.14;
    char   c = 'Z';

    push(s, &a);
    push(s, &b);
    push(s, &c);

    // pop them back ─ WE remember the real type of each one
    cout << *(char *)  pop(s) << endl;   // Z
    cout << *(double *)pop(s) << endl;   // 3.14
    cout << *(int *)   pop(s) << endl;   // 42

    return 0;
}

The output will be ─

Z
3.14
42

🥳🥳 A single stack implementation just held an int, a double, and a char at the same time! This is precisely how generic containers were built in C for decades ─ linked lists, queues, hash tables, trees ─ all storing void* so one implementation serves every type. The catch, of course, is that we must remember what each box really holds (notice we cast each pop to the correct type). The container trusts us completely 🤔

Real problem 7 ─ raw bytes: copying and saving anything 💾

Sometimes we don’t care what the data is at all ─ we just want to shovel its bytes somewhere. Copying memory, saving a struct to disk, sending it over a network ─ these treat an object as a plain blob of bytes. And “a blob of bytes at an address” is exactly a void pointer.

That’s why memcpy takes void* for both source and destination ─

#include <iostream>
#include <cstring>
using namespace std;

int main()
{
    int source = 2049;
    int dest   = 0;

    // memcpy sees pure bytes ─ void* dest, const void* src, byte count
    memcpy(&dest, &source, sizeof(int));

    cout << "dest = " << dest << endl;

    return 0;
}

The output will be ─

dest = 2049

🥳 memcpy (and its friends memset and memmove) don’t know or care that these are ints ─ they just copy sizeof(int) raw bytes from one address to another. This same idea is how we serialise a struct to a file with fwrite, or blast a buffer across a socket ─ the data becomes a typeless stream of bytes, described by a void* and a length. When “the type doesn’t matter, only the bytes do,” the void pointer is the natural fit.

Real problem 8 ─ opaque handles: hiding the details 🔒

One more real-world pattern worth knowing. Library authors often want to hand us a “handle” to something ─ a database connection, a texture, a file ─ without revealing its internal structure. A clean way to do that is to give us a void* (or a pointer to an incomplete type) and a set of functions that accept it.

We never dereference the handle ourselves; we just pass it back to the library’s functions, which know the real type internally. It’s the ultimate unlabeled box ─ we‘re not even meant to open it 📦. This keeps the library free to change its internals without breaking our code, and it’s how many C APIs (and OS handles) are designed.


The modern C++ angle ─ templates and type erasure 🧭

By now you might be thinking “this is powerful, but casting the wrong label sounds dangerous…” ─ and you’re absolutely right 🥳. The void pointer trusts us completely, and if we put the wrong label on a box, nobody stops us.

So what does modern C++ do? It gives us safer ways to be generic ─ but they’re built on the very idea we just learned.

Templates are the first answer. Remember vector<int> vs vector<double> from the STL posts? Templates let us write one piece of code that the compiler stamps out for each type ─ with full type checking. It’s the “works on any type” power of the void pointer, minus the risk of a wrong cast, because the compiler knows the real type the whole time.

// a type-safe generic swap ─ no void*, no casts, fully checked
template <typename T>
void mySwap(T &a, T &b)
{
    T temp = a;
    a = b;
    b = temp;
}

Type erasure is the second answer, and here the void pointer is often hiding right underneath! Tools like std::any (holds a value of any type), std::function (holds any callable), and std::shared_ptr<void> (a smart pointer to anything) all give us that “store anything” flexibility ─ but wrapped in a safe, checked interface. Under the hood, many of these use a void* plus some bookkeeping to remember the real type. So the void pointer never went away; C++ just put safety rails around it 😉

Here’s the honest, practical guidance ─

Situation Reach for
We control the code and know the types at compile time Templates ─ type-safe, zero overhead
We need to store “any type” and check it safely at run time std::any / std::variant
We need to store “any callable” (functions, lambdas) std::function
We’re talking to a C library, a Unix API, or writing very low-level code void* ─ the right tool for the job

So void* isn’t obsolete ─ it’s the low-level foundation. We’ll use it whenever we cross into C, into the operating system, or into the raw-bytes world. And understanding it is exactly what makes templates and std::any feel like the gifts they are 🥳

Gotchas to keep us safe ⚠️

Void pointers are sharp tools, so let’s gather every tripwire in one place ─

  • We can’t dereference a void* directly. Always cast to a real type first ─ *( (int *)ptr ), never *ptr.
  • No pointer arithmetic on a void*. There’s no element size, so ptr + 1 is meaningless. Cast first, then step.
  • The label must be correct. Casting a void* to the wrong type and dereferencing gives us garbage or undefined behaviour ─ like reading a four-byte number one byte at a time. The pointer trusts us to remember what’s really inside the box 📦.
  • sizeof(void*) is fine, but sizeof(*ptr) is not. We can ask how big the pointer is, but not how big the thing it points to ─ because it doesn’t know!
  • Never delete through a void*. If we allocated an object with new, delete it through its real type ─ deleting via a void pointer is undefined behaviour, because the wrong destructor (or none) may run.
  • A void* can still be nullptr. Typelessness doesn’t make it safe from null ─ check it points somewhere valid before we cast and dereference, just like any pointer.
  • Function pointers are a special case. Strictly speaking, C++ doesn’t guarantee a void* can hold a function pointer ─ that’s a separate family. (Unix’s dlsym relies on it working in practice, but keep the distinction in mind for portable code.)

Follow these and the void pointer is a friendly, powerful helper. Ignore them and it will happily hand you nonsense with a straight face 😅

So what should we remember? 🤔

Let’s wrap up the key takeaways ─

  • A void pointer (void *ptr) is a typeless / generic pointer that can store the address of any type.
  • Ordinary pointers are strongly typed because the type tells them how many bytes to read and how to interpret them when dereferencing.
  • A void pointer deliberately throws that type away ─ so it knows where the data is, but not what it is (the unlabeled box on the shelf 📦).
  • Because of that, we cannot dereference a void* directly ─ we must cast it to a real pointer type first (*( (int *)ptr )). The cast is putting the label back on the box.
  • Storing into a void* needs no cast; casting back out to a specific type always does (and C++ is stricter about this than C).
  • The same address read through different labels can give different values ─ proof that the type decides how much memory is read (our 20491 example).
  • No arithmetic on a void* ─ there’s no element size until we cast it to a type.
  • Its big purpose is generic, type-independent code, and it’s everywhere in real software ─
    • malloc returns a void*; memcpy/memset take void* (raw bytes).
    • qsort/bsearch sort and search arrays of any type through void*.
    • Unix systems programmingpthread_create/pthread_join pass our data in and out as void*/void**; read/write/mmap/dlsym all speak void*.
    • Callbacks use the void *userdata “context pointer” pattern (GTK, libcurl, SDL, and passing a C++ this through a C callback).
    • Generic data structures (stacks, lists, hash tables) store void* so one implementation serves every type.
    • Opaque handles hide a library’s internals behind a typeless pointer we never open ourself.
  • In modern C++ we usually prefer templates (compile-time, type-safe) or type erasure (std::any, std::function, std::shared_ptr<void>) ─ but these are built on the void-pointer idea, with safety rails added.
  • Handle with care ─ wrong label, arithmetic, delete through a void*, or a stray nullptr all lead to undefined behaviour.

Once these click, the “typeless” pointer stops feeling like a loophole and starts looking like exactly what it is ─ a raw address, waiting for us to tell it what it means 👀✨

Congratulations 🥳🥳🥳
We now understand void pointers, top to bottom ─ the generic pointer that quietly powers memory allocators, sorting routines, Unix thread APIs, callbacks, generic containers, and even the guts of modern C++. That’s a big one checked off 💪

In the next post we will talk about const pointers ─ what happens when we start locking things down with const, and untangling that wonderfully confusing difference between a pointer to a constant and a constant pointer 😉

Happy Coding 💻 🎵

Leave a Comment

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