Hi everyone ✋
Over this whole series we’ve learned to love pointers ─ how they hold addresses, walk arrays, reach into objects, manage memory, and even name members in the abstract. Today we turn the lamp on the dark side 🌑.
Every scary story you’ve heard about C++ ─ mysterious crashes, corrupted data, “it worked yesterday” bugs ─ almost always traces back to two culprits ─ wild pointers and dangling pointers. They’re the pointers that lead somewhere they shouldn’t, and they cause the most notorious bugs in the language.
If we haven’t read the earlier posts in this series, it’s worth going through them first ─ today pulls together the dynamic memory, smart pointers, and NULL vs nullptr posts especially.
Today’s topic is dangling and wild pointers ─ where they come from, why they’re so dangerous, and the handful of simple habits that keep our pointers safe. This is the post that turns “C++ is scary” into “C++ is fine, once the rules are clear” 🥳.
Let’s take a deep dive 🦴
Three states a pointer can be in 🚦
Before naming the villains, let’s line up all the states a pointer can be in ─
- Valid ─ it holds the address of a real, living object. Following it is perfectly safe. 🟢
- Null ─ it deliberately holds no address, and says so honestly (our nullptr friend). We can’t follow it, but we can check it with
if (ptr), so it’s safe. 🟡 - Invalid ─ it holds an address that looks real but leads to garbage or freed memory. Following it is undefined behaviour ☠️. 🔴
That last state is the dangerous one, and it comes in two flavours ─ a pointer that was never given a real address (wild), and a pointer whose target has ceased to exist (dangling). The whole trick to safe pointers is keeping every pointer in one of the two safe states and out of the red. Let’s picture it.
My little reference ─ the slip that points at rubble 🏚️
Let’s keep the house-and-address picture that’s carried us all series. A pointer is a slip of paper with a house address; following it means driving to that house.
- A valid pointer is a slip with the address of a real, standing house. 🟢
- A null pointer is a slip that plainly reads “no address.” We won’t drive anywhere, and that’s fine. 🟡
- A wild pointer is a slip with a random address scribbled on it that we never actually set ─ some leftover number. It might point at an empty lot, a stranger’s house, or the middle of a lake 🌊. We simply never wrote a real address on it. 🔴
- A dangling pointer is the sneakiest of all ─ a slip pointing at a house that has since been demolished 🏚️. The address still looks perfectly valid, but if we drive there, we find rubble ─ or worse, a brand-new house someone else built on the empty lot, whose belongings we now rummage through by mistake.
That last image is the key to the whole post. A dangling slip looks identical to a good one ─ same neat address, no warning label. That’s exactly why these bugs are so hard to catch. Keep the rubble in mind 😉.
Wild pointers ─ never given a real address 🎲
A wild pointer is simply an uninitialized pointer. We declared it, but never pointed it at anything ─ so it holds whatever garbage happened to be in that memory. Using it is a gamble with terrible odds.
int *p; // uninitialized ─ holds a leftover garbage address *p = 42; // ☠️ wild pointer ─ writing to who-knows-where
The compiler often spots this and warns us ─
warning: 'p' is used uninitialized [-Wuninitialized]
The fix is the easiest habit in this whole post ─ never leave a pointer uninitialized. Give it a real address right away, or set it to nullptr so it lands in the safe “no address” state ─
int *p = nullptr; // ✅ a known, safe "no address" ─ never wild
Now p is honest and checkable. If we accidentally use it before pointing it somewhere real, an if (p) guard catches it, instead of a random write silently trashing memory. One tiny = nullptr turns a landmine into a speed bump 🥳.
Dangling pointers ─ the house was demolished 🏚️
A dangling pointer is trickier ─ it was valid, but its target has been destroyed while the pointer kept holding the old address. There are three classic ways to create one, and they’re worth knowing by name.
1 ─ Returning a pointer to a local variable. A local lives only until its function returns, so handing back its address gives the caller a slip to a demolished house.
int* makeNumber()
{
int x = 42;
return &x; // ☠️ x is destroyed the moment we return
}
Thankfully the compiler usually catches this one ─
warning: address of local variable 'x' returned [-Wreturn-local-addr]
The fix is to return the value, not its address (or, if we truly need a pointer, allocate on the heap and manage its lifetime) ─
int makeNumber() { return 42; } // ✅ return the value itself
2 ─ Use-after-free. This is the big one from the dynamic memory post ─ we delete heap memory, but keep using the pointer that pointed at it.
int *p = new int(42); delete p; // the house is demolished ─ memory returned to the system // *p = 10; // ☠️ use-after-free ─ p now points at rubble p = nullptr; // ✅ cross out the address so it can't be followed
The habit that saves us ─ after delete, set the pointer to nullptr. The slip now honestly reads “no address,” and any later if (p) guard will skip it.
3 ─ Double delete. Deleting the same pointer twice tries to demolish an already-demolished house ─ undefined behaviour. But the nullptr habit makes even this safe, because deleting a null pointer is a guaranteed no-op ─
#include <iostream>
using namespace std;
int main()
{
int *p = new int(42);
delete p; // free it once
p = nullptr; // and immediately null it
delete p; // ✅ deleting nullptr is a harmless no-op
cout << "no double-delete crash" << endl;
return 0;
}
The output will be ─
no double-delete crash
Notice the theme ─ that single p = nullptr; after every delete quietly defuses two different bugs at once. It’s the cheapest insurance in C++.
A subtler dangler ─ pointing into a container 📦
Here’s one that catches even experienced developers, and it ties straight back to the vector post. When we hold a pointer into a container that can grow, adding elements may force the container to reallocate its storage ─ moving everything to a new location and freeing the old block. Any pointer into the old block instantly dangles.
#include <vector>
using namespace std;
vector<int> v = { 1, 2, 3 };
int *p = &v[0]; // pointer into the vector's current storage
v.push_back(4); // may reallocate ─ the storage can move elsewhere
// *p = 99; // ☠️ p may now point at freed memory
The lesson ─ be wary of storing raw pointers (or references, or iterators) into containers that might resize. If we need to keep referring to an element across insertions, store an index instead, or re-fetch the pointer after the container settles.
Dangling references ─ the same bug in disguise 🪞
Everything we’ve said about dangling pointers applies to references too. A reference is really just a pointer wearing friendlier clothes (as we saw in the pass by value and pass by reference post), so it can dangle in exactly the same way. The classic slip is returning a reference to a local ─
int& badRef()
{
int x = 42;
return x; // ☠️ returning a reference to a local ─ x dies on return
}
The compiler flags it just like the pointer version ─
warning: reference to local variable 'x' returned [-Wreturn-local-addr]
The danger is identical ─ the caller ends up bound to a demolished house. So the rule extends cleanly ─ never hand back a reference or a pointer to something that’s about to disappear.
Why they’re so dangerous ─ the bug that hides 😱
Here’s what makes wild and dangling pointers uniquely nasty. Following one is undefined behaviour ─ which does not reliably mean “instant crash.” It means anything can happen, and the most cruel outcome is that it often seems to work.
That freed memory might not be reused for a while, so *p returns the old value today and passes every test. Then next week, a new allocation lands on that exact spot ─ a fresh house on the old lot ─ and suddenly our dangling write is silently corrupting someone else’s data. The crash, when it finally comes, is nowhere near the real bug. These are the infamous “works on my machine” and “crashes only in production” ghosts 👻.
That’s the real reason we treat pointer hygiene so seriously. A null-pointer bug usually crashes immediately, right at the scene. A dangling-pointer bug hides, travels, and detonates later ─ so our goal is to never create one in the first place.
A real trap ─ two slips to one house 🏠🏠
Here’s a bug that shows exactly why we take all this so seriously ─ and it’s the very reason smart pointers exist. Picture a small class that owns a raw pointer and cleans it up in its destructor ─
#include <iostream>
using namespace std;
class Box
{
public:
int *data;
Box(int v) { data = new int(v); } // grab some heap memory
~Box() { delete data; } // and free it on the way out
};
int main()
{
Box a(42);
Box b = a; // ☠️ the default copy duplicates the POINTER
cout << *b.data << endl;
return 0; // both destructors delete the SAME pointer
}
When we wrote Box b = a;, C++ made a shallow copy ─ it duplicated the data pointer, so now a.data and b.data are two slips pointing at one house 🏠🏠. Everything looks fine ─ it even prints the right answer ─
42
…and then, as main ends, both destructors run and both call delete on that one shared address. The second delete is a double-free, and the program aborts on the way out with something like ─
free(): double free detected ...
There’s the whole horror in miniature ─ the program worked, printed the correct value, then detonated at exit, nowhere near the line that actually caused it. The fix is to make ownership explicit ─ give the class a proper deep copy (the “Rule of Three”), or, far better, let a smart pointer own the memory so the sharing is handled correctly. That’s the perfect thread to carry into next time 🧵.
A tool that catches them ─ AddressSanitizer 🔍
We don’t have to hunt these by eye. Modern compilers ship with AddressSanitizer (ASan) ─ turn it on and it watches every access at runtime, stopping the instant we touch freed or invalid memory. Compile with one extra flag ─
g++ -std=c++17 -fsanitize=address -g bug.cpp -o bug
Run a program with a use-after-free, and instead of a mysterious later crash, ASan points at the exact line ─
==ERROR: AddressSanitizer: heap-use-after-free ... SUMMARY: AddressSanitizer: heap-use-after-free bug.cpp:5 in main
That’s the bug caught at the moment it happens, with a file and line number. Valgrind is another classic tool that does similar detective work. Whenever a pointer bug feels impossible to pin down, reach for one of these ─ they turn invisible ghosts into a plain address and a line number 🥳.
The real fix ─ let ownership manage lifetime 🛡️
Every habit above is good, but there’s a bigger idea that makes most of these bugs impossible rather than merely avoidable ─ smart pointers, from the smart pointers post.
A smart pointer ties an object’s lifetime to a clear owner. When the owner goes away, the object is freed ─ automatically, exactly once ─ so there’s no forgotten delete (no leak), no lingering raw pointer (no dangling), and no second delete (no double-free).
#include <memory>
using namespace std;
int main()
{
auto p = make_unique<int>(42); // owns the int
*p = 10;
// no delete needed ─ freed automatically when p goes out of scope
// nothing to dangle, nothing to double-free
return 0;
}
The modern guidance is simple ─ prefer smart pointers for ownership, use raw pointers only for observing (not owning), and keep the small habits below for the raw pointers that remain.
Gotchas to keep us safe ⚠️
Let’s gather every rule in one place ─
- Never leave a pointer uninitialized. Point it at something real, or set it to
nullptr. No wild pointers. - After
delete, set the pointer tonullptr. This kills use-after-free and double-delete in one move, sincedelete nullptris a safe no-op. - Never return a pointer or reference to a local variable. It dangles the instant the function returns ─ return by value instead. References dangle exactly like pointers.
- Watch out when a class owns a raw pointer. The default copy is shallow, leaving two objects sharing (and double-deleting) one allocation ─ give it a proper copy (Rule of Three) or hand ownership to a smart pointer.
- Match every
newwith exactly onedelete(andnew[]withdelete[]). Don’t delete twice, don’t forget. - Be careful pointing into containers that can resize. A
vectorreallocation dangles pointers, references, and iterators ─ store an index if you need to hold on. - Guard with
if (ptr)before dereferencing any pointer that might be null. - Reach for AddressSanitizer or Valgrind when a memory bug won’t reveal itself.
- Prefer smart pointers for ownership. They make most of these bugs structurally impossible.
Follow these and the “scary” reputation of C++ pointers quietly melts away 🥳.
So what should we remember? 🤔
Let’s wrap up the key takeaways ─
- A pointer is in one of three states ─ valid (safe 🟢), null (safe, because checkable 🟡), or invalid (dangerous ☠️ 🔴). The invalid state is what we fight.
- Picture the slip pointing at rubble 🏚️ ─ a wild pointer holds a random address we never set; a dangling pointer holds a valid-looking address to a house that’s been demolished.
- Wild pointers come from leaving a pointer uninitialized ─ fix it by always initialising to a real address or
nullptr. - Dangling pointers come from returning a pointer to a local, use-after-free, double delete, and pointing into a container that reallocates ─ and references dangle the same way.
- A class that owns a raw pointer is a classic trap ─ the default shallow copy leaves two objects deleting one allocation, which is why we reach for the Rule of Three or a smart pointer.
- The
p = nullptr;habit afterdeletedefuses both use-after-free and double-delete, because deletingnullptris a guaranteed no-op. - These bugs are dangerous because they’re undefined behaviour that often seems to work, then corrupts memory or crashes far from the real cause.
- AddressSanitizer and Valgrind catch them at the exact moment and line they happen.
- The structural fix is smart pointers ─ tie lifetime to ownership and most of these bugs simply can’t occur.
Once these habits are second nature, pointers stop being scary ─ every slip either points at a real house or honestly says “no address,” and never at rubble 👀✨.
Congratulations 🥳🥳🥳
We’ve now faced the dark side of pointers ─ and come away with a small, powerful set of habits that keep every pointer safe.
In the next post we’ll return to smart pointers for a deeper look ─ weak_ptr, breaking reference cycles, custom deleters, and the finer points of shared_ptr ownership ─ the modern tools that make the bugs from today almost impossible to write 😉
Happy Coding 💻 🎵