A dangling pointer holds the address of memory that has been freed. The address is still there and still looks valid; what it points at is no longer yours.

The reason this is worth a whole post is that dereferencing one usually works. The allocator has not necessarily reused the memory yet, so the old value is often still sitting there. The program reads it, gets the right answer, and carries on — until the day something else gets allocated into that space and the same read returns garbage. The bug has been there the whole time; only the symptom is new.

What it looks like

dangling.cpp
#include <iostream>
 
int* createInt() {
	return new int(5);
}
 
int main() {
	int* ptr = createInt();
 
	delete ptr;          // memory returned to the allocator
	int value = *ptr;    // undefined behaviour
 
	std::cout << value << '\n';
	return 0;
}

After delete, ptr still holds the same address. Nothing about the pointer changed — only the ownership of what it refers to.

CAUTION

This program will very likely print 5. That is the worst possible outcome, because it suggests the code is fine.

Undefined behaviour does not mean "crashes". It means the standard imposes no requirement whatsoever on what happens, and compilers are entitled to optimise on the assumption that it never occurs. A build that works today can break on a compiler upgrade with no source change.

The four ways you get one

Using a pointer after freeing it. The case above. The pointer outlives what it points to, and nothing in the language stops you dereferencing it.

Returning a pointer to a local. The variable's storage ends with the function, so the caller receives an address into a stack frame that no longer exists:

int* broken() {
	int x = 42;
	return &x;  // x dies here
}

Compilers usually warn about this exact shape. They cannot warn about the version where the address escapes through a struct field or a lambda capture.

Double free, or freeing in the wrong order. Releasing the same allocation twice corrupts the allocator's own bookkeeping, which tends to produce a crash somewhere entirely unrelated to the bug.

Sharing across threads. One thread frees while another is still reading. This has all the properties of the single-threaded case plus non-determinism, which makes it substantially worse to reproduce.

There is also a subtler variant that catches people who think they have avoided all of the above: holding a pointer or reference into a container that then reallocates. std::vector invalidates pointers to its elements when it grows, so a perfectly ordinary push_back can dangle a pointer you took ten lines earlier.

The manual fix, and why it is not enough

Set the pointer to nullptr after freeing, and check before use:

safer.cpp
void release(int*& ptr) {
	delete ptr;
	ptr = nullptr;
}
 
int main() {
	int* ptr = createInt();
	release(ptr);
 
	if (ptr != nullptr) {
		std::cout << *ptr << '\n';
	} else {
		std::cout << "pointer released\n";
	}
	return 0;
}

Taking the pointer by reference (int*&) is what makes this work — it nulls the caller's pointer, not a copy of it.

This is a real improvement: a null dereference fails immediately and loudly, which beats reading stale data silently. But it only protects the one pointer you nulled. Any other pointer to the same allocation is still dangling, and this technique cannot help you find them.

What actually solves it

Stop managing lifetimes by hand.

auto ptr = std::make_unique<int>(5);
// freed automatically when ptr goes out of scope

std::unique_ptr for single ownership, std::shared_ptr where ownership is genuinely shared, and std::weak_ptr to observe without keeping alive. The destructor runs at the right time by construction, which removes the entire class of bug rather than making it easier to spot.

The general principle is RAII: tie the lifetime of a resource to the lifetime of an object, and let scope do the bookkeeping. It applies to files, locks, and sockets exactly as it does to memory.

Modern C++ style follows from this. Prefer values to pointers. Prefer containers to manual arrays. Use raw pointers only as non-owning observers, and only where the owner provably outlives them.

Tools that find what discipline misses

Discipline does not scale to a codebase with several authors and a decade of history. Instrumentation does:

AddressSanitizer is the highest-value tool here by a wide margin. It catches use-after-free, double free, and out-of-bounds access at the moment they happen, with a stack trace of both the access and the original deallocation:

g++ -fsanitize=address -g dangling.cpp -o dangling && ./dangling

It costs roughly 2× runtime and is worth running your whole test suite under.

Valgrind finds similar problems without recompiling, considerably more slowly.

Static analysers — clang-tidy, cppcheck — catch the obvious patterns at build time, before anything runs.

None of these is complete, and ASan in particular only reports bugs on code paths your tests actually execute. Together with smart pointers they cover most of the realistic ground.

The underlying point

Dangling pointers are not a flaw in C++ so much as the direct consequence of a language that lets you manage memory manually. That control is the reason to use C++ at all, and it comes with the obligation to know what owns what.

The good news is that the obligation is mostly discharged by choosing better defaults. Ownership expressed in types is checked by the compiler; ownership held in the programmer's head is checked by nobody.