Pointers

Dangling and Uninitialized Pointers

As we learned already the only thing a pointer can ever do is hold an address. There is nothing within the pointer that in any way guarantees that the memory at said address still lives, or that the memory was ever even valid. When such a guarantee is absent, the pointer is considered uninitialized or dangling, and any subsequent usage of it results in UB.

Uninitialized pointer. An uninitialized pointer has no address in it when created. The value of the bits in the pointer are not defined, but whatever was in the storage location, whether on the stack or the heap, is what will be in that pointer. Dereferencing such a pointer equals:

#include <iostream>

int main()
{
    int* ptr;

    // std::cout << *ptr << '\n'; // Undefined behavior

    return 0;
}

The variable, ptr, is declared but never assigned a value. Its value is undefined. The commented one line instruction would instruct the program to interpret those unknown bits as the address of an int and then fetch whatever resides at that location. The language places no restrictions on what occurs after that; it may crash, output random data, or seem to run correctly until a later run of the program fails.

One of the easiest ways to avoid the problem is to initialize the pointer to a known value at the time of creation. Null pointer is the conventional value to use, if no actual object is available at the time.

#include <iostream>

int main()
{
    // Use nullptr when the pointer refers to no object
    int* ptr { nullptr };

    if (ptr)
    {
        std::cout << *ptr << '\n';
    }

    return 0;
}

Here, ptr is set to null. The test if (ptr) will be false, and dereference is not attempted. This program is safe. An uninitialized pointer provides no such test, since its value is unknown.

A dangling pointer is distinct. At some point, a valid address was stored there, but the object residing at that address has been destroyed. It still has the formerly valid address, but the address isn't valid anymore.

The most frequent cause of a dangling pointer is when it refers to a local variable that later goes out of scope.

#include <iostream>

int* getPointer()
{
    int local { 42 };

    return &local;
}

int main()
{
    int* ptr { getPointer() };

    // std::cout << *ptr << '\n'; // Undefined behavior

    return 0;
}

Within getPointer, an int local is created and its address is returned. When the function returns, local dies and this space is available for other storage. The pointer ptr that we return to the main still has the space of local in it, but the compiler can do what it wants when dereferencing a pointer.

In this case, a dereference happens after local died and therefore it tries to read memory that the program no longer owns. This is, once again, undefined behavior.

The same occurs if a pointer outlives an allocated object that has been freed.

#include <iostream>

int main()
{
    int* ptr { new int{ 99 } };

    delete ptr;

    // std::cout << *ptr << '\n'; // Undefined behavior

    return 0;
}

After delete ptr, the memory is released to the free store. The pointer variable still holds the address as a number, but it is no longer a valid address; the use of it for a read or a write is undefined behavior. It is safe to nullify the pointer immediately after a delete to stop this invalid behavior.

#include <iostream>

int main()
{
    int* ptr { new int{ 99 } };

    // Release the dynamically allocated object
    delete ptr;
    ptr = nullptr;

    if (ptr)
    {
        std::cout << *ptr << '\n';
    }

    return 0;
}

When ptr is null, the test guards against the subsequent code. The address that has been freed can't be accidentally used any longer.

Uninitialized and dangling pointers share the same underlying flaw: the integer value stored in the pointer does not address a live object of the type expected. There is no reason for the compiler to diagnose either at the point of use: the bug manifests only when the code dereferences the erroneous address, by which time havoc might already be invisible or irremediable.

Because a pointer is simply a number, the language offers no built in check to see if the memory it refers to still exists.

The programmer must guarantee that all pointers are initialized to a known value before use, and null, or reinitialized to another value, immediately if or when the target object ceases to exist. When those disciplines are followed, the pointer stays a solid handle; when they are not, the program enters a realm of undefined behavior with no support forthcoming from the compiler.

Exercise: Dangling and Uninitialized PointersStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Dangling and Uninitialized Pointers

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Print the allocated value while it is still alive, then retain the delete and nullptr assignment. Report whether ptr is empty afterward without dereferencing it.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <iostream>

int main()
{
    int* ptr { new int{ 99 } };

    // Release the dynamically allocated object
    delete ptr;
    ptr = nullptr;

    if (ptr)
    {
        std::cout << *ptr << '\n';
    }

    return 0;
}

Hints

Hint 1

delete ptr ends the allocated int's lifetime; setting ptr to nullptr then clears this particular pointer variable. Clearing a pointer is not what frees the allocation.

Hint 2

The final if (ptr) must be false here, so no dereference occurs. Do not remove that protection to inspect a deleted object or expect a predictable result.

Worked solution

The allocated 99 is now printed before delete while the object is alive. After delete, ptr is cleared and only compared with nullptr. No value is read through the old address.

#include <iostream>

int main()
{
    int* ptr { new int{ 99 } };

    // Release the dynamically allocated object
    std::cout << "Before release: " << *ptr << '\n';
    delete ptr;
    ptr = nullptr;

    if (ptr)
    {
        std::cout << *ptr << '\n';
    }

    std::cout << "Cleared pointer: " << (ptr == nullptr) << '\n';
    return 0;
}

Example output

Before release: 99
Cleared pointer: 1

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.