Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 5 of 7

A dangling pointer still holds an address, but the object that was there is gone. The pointer looks perfectly valid. Nothing about it says "expired."

Using one is use-after-free, and it is among the most serious bug classes in C++ — the foundation of a large share of real-world security vulnerabilities.

Three ways to make one

1. Delete the object, keep the pointer.

int* p = new int(42);
delete p;
// p still holds the old address. The memory belongs to the allocator again.
std::cout << *p;      // use-after-free: undefined behaviour

2. Return the address of a local.

int* make_dangling() {
    int local = 42;
    return &local;        // local dies when the function returns
}

This one the compiler does catch:

warning: address of local variable 'local' returned [-Wreturn-local-addr]

Note "warning" — the program still compiles and runs. The same applies to returning a reference to a local, which produces the matching reference to local variable 'x' returned.

3. Keep a pointer into a container that reallocates.

std::vector<int> v = {1, 2, 3};
int* p = &v[0];
v.push_back(4);       // may move the whole buffer to a new address
std::cout << *p;      // p may now point at freed memory

This third one surprises people, because no delete appears anywhere. Growing a std::vector past its capacity allocates a bigger buffer, copies the elements, and frees the old one — so every pointer, reference and iterator into it can become dangling.

Watching a pointer go stale, safely

You can observe the pointer keeping its value after the object dies without ever dereferencing it:

#include <iostream>

int main() {
    int* p = new int(42);
    int* remembered = p;

    std::cout << "before delete, *p = " << *p << "\n";

    delete p;
    p = nullptr;                          // good practice: mark it dead

    // `remembered` was never cleared, so it still holds the old address.
    std::cout << "p is null now:          " << (p == nullptr) << "\n";
    std::cout << "remembered is not null: " << (remembered != nullptr) << "\n";
    std::cout << "and still holds the freed address, indistinguishable from a live one\n";

    // Dereferencing `remembered` here would be use-after-free. We do not.
    return 0;
}
before delete, *p = 42
p is null now:          1
remembered is not null: 1
and still holds the freed address, indistinguishable from a live one

That is the heart of the problem: a dangling pointer and a valid pointer are the same eight bytes. No check you can write distinguishes them.

Why nothing warns you

Plain compilation is silent on all of this:

int* p = new int(1);
delete p;
int value = *p;        // no diagnostic from g++ -Wall -Wextra
delete p;              // double delete: also no diagnostic

The compiler cannot know at compile time which pointers are still live — that depends on run-time control flow. So it says nothing, and the bug reaches production.

What does catch it is a run-time tool. Build with AddressSanitizer:

g++ -std=c++20 -fsanitize=address -g main.cpp -o main

and the same program reports:

ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010
READ of size 4 at 0x502000000010 thread T0
    #0 0x... in main main.cpp:2

The exact operation, the exact line. Module 11 covers the sanitizers properly; for now, know that this class of bug is invisible to the compiler and obvious to ASan.

Habits that help

Set pointers to nullptr after deleting. A null dereference crashes immediately and predictably; a dangling dereference corrupts silently. Note this only helps the pointer you nulled — copies elsewhere are unaffected, as the example above showed.

Never return the address of a local. Return by value, or take an output parameter.

Assume any pointer into a vector is invalidated by anything that can grow it. push_back, insert, resize, reserve. Store an index instead, which survives reallocation:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3};

    std::size_t index = 0;       // an index, not a pointer

    for (int i = 0; i < 100; ++i) {
        v.push_back(i);          // may reallocate many times
    }

    // Still correct: the index does not care where the buffer lives now.
    std::cout << "v[" << index << "] = " << v[index] << "\n";
    std::cout << "size = " << v.size() << "\n";
    return 0;
}
v[0] = 1
size = 103

Best of all: do not hold raw owning pointers. With std::vector, std::string and std::unique_ptr, the object's lifetime is tied to a scope, so there is no window in which a pointer can outlive it.

The scope version of the same bug

#include <iostream>

int main() {
    int* p = nullptr;

    {
        int local = 42;
        p = &local;
        std::cout << "inside the block:  " << *p << "\n";
    }   // local dies here

    // *p here would be undefined behaviour — the stack slot may already be reused.
    std::cout << "after the block, p is stale; we do not dereference it\n";
    return 0;
}
inside the block:  42
after the block, p is stale; we do not dereference it

No new and no delete, and still a dangling pointer. Stack memory is reclaimed at the closing brace just as surely as heap memory is at delete.

Try it

Write a function that returns a pointer to a local variable, compile it, and read the warning. Then dereference the result and see what your program prints. Finally rebuild it with -fsanitize=address and compare.

Checkpoint

  • A dangling pointer holds the address of an object that no longer exists.
  • Using one is use-after-free — undefined behaviour and a major security bug class.
  • You get one by deleting and keeping the pointer, returning a local's address, or holding a pointer into a vector that grows.
  • A dangling pointer is byte-for-byte indistinguishable from a valid one.
  • The compiler catches only the returned-local case, and only as a warning.
  • AddressSanitizer catches the rest at run time, with an exact line.
  • Set pointers to nullptr after deleting; store indices rather than pointers into containers.

Questions about this lesson

Log in to ask a question.