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 you instantly.
My little reference ─ the unlabeled box on a warehouse shelf 📦
This is the mental picture I want you 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 you 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 you the contents. There’s simply not enough information.
The fix ─ label the box before you 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 anint.” This is the label going back on the box.*( (int *)ptr )─ now that it’s anint*, dereferencing is legal again ─ read 4 bytes, interpret as an int, and out comes15.
So a void pointer is a two-step dance ─ store freely, but cast before you 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 you 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 number2049. - When we cast to
char*, it read only the first single byte ─ which was00000001─ so we got1.
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 you’re going to store; it just reserves the raw bytes and hands you 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”), you tell it the element size, and you hand it a little comparator function. That comparator receives two const void* ─ and you 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 you start a thread, you 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 your function with your data ─ whatever type that data happens to be. The library authors had no idea what you’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 you want into a struct, pass its address as a void*, and inside the thread, relabel the box back to your 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 your 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 you 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()andwrite()─ these syscalls take avoid *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 avoid*to the start of it. You decide what type lives there.dlopen()/dlsym()─ for loading plugins/shared libraries at run time,dlsymhands you back avoid*to a symbol you then cast to the right function or variable type.- Signal handlers ─ advanced signal handling (
sigactionwithSA_SIGINFO) passes avoid *contextdescribing the interrupted state.
Every one of these is the same story ─ a general-purpose interface that can’t possibly know your 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 you see it you’ll spot it in every library you touch.
Imagine you register a callback ─ “when this button is clicked, call my function.” But your function often needs some context ─ which counter to increment, which object to update. The library can’t know what context you need. So the classic solution is a void *userdata slot ─ you stash any data you like, and the library faithfully hands it back to your 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 you 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 you a void* userdata, you can pass your object’s this pointer through it! Inside a plain callback function, you cast the void* back to your 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 you want a stack. Do you write IntStack, DoubleStack, CharStack, StudentStack…? That’s madness. Instead, 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 you must remember what each box really holds (notice we cast each pop to the correct type). The container trusts you 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 you 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 you 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 you a void* (or a pointer to an incomplete type) and a set of functions that accept it.
You never dereference the handle yourself; you just pass it back to the library’s functions, which know the real type internally. It’s the ultimate unlabeled box ─ you’re not even meant to open it 📦. This keeps the library free to change its internals without breaking your 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 you completely, and if you put the wrong label on a box, nobody stops you.
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 you 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 |
|---|---|
| You control the code and know the types at compile time | Templates ─ type-safe, zero overhead |
| You need to store “any type” and check it safely at run time | std::any / std::variant |
| You need to store “any callable” (functions, lambdas) | std::function |
| You’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. You’ll use it whenever you 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 you safe ⚠️
Void pointers are sharp tools, so let’s gather every tripwire in one place ─
- You 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, soptr + 1is meaningless. Cast first, then step. - The label must be correct. Casting a
void*to the wrong type and dereferencing gives you garbage or undefined behaviour ─ like reading a four-byte number one byte at a time. The pointer trusts you to remember what’s really inside the box 📦. sizeof(void*)is fine, butsizeof(*ptr)is not. You can ask how big the pointer is, but not how big the thing it points to ─ because it doesn’t know!- Never
deletethrough avoid*. If you allocated an object withnew, 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 benullptr. Typelessness doesn’t make it safe from null ─ check it points somewhere valid before you 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’sdlsymrelies 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, you cannot dereference a
void*directly ─ you 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
2049→1example). - No arithmetic on a
void*─ there’s no element size until you cast it to a type. - Its big purpose is generic, type-independent code, and it’s everywhere in real software ─
mallocreturns avoid*;memcpy/memsettakevoid*(raw bytes).qsort/bsearchsort and search arrays of any type throughvoid*.- Unix systems programming ─
pthread_create/pthread_joinpass your data in and out asvoid*/void**;read/write/mmap/dlsymall speakvoid*. - Callbacks use the
void *userdata“context pointer” pattern (GTK, libcurl, SDL, and passing a C++thisthrough 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 you never open yourself.
- 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,
deletethrough avoid*, or a straynullptrall 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 you 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 💻 🎵