Dynamic Memory and RAII

Why Modern C++ Avoids Manual Memory Management

Important: This lesson uses classes in the code examples which we haven't covered yet, so you can SKIP it until you learn about classes, in the Classes and Objects category.

Every new call creates an obligation that has to be discharged with an associated delete at some later time. If the obligation is discharged by hand, every execution path of the program has to be correct. One forgetful statement, an early return, or an extra delete is sufficient to result in a leak, a double deletion, or a use after free error.

Consequently, modern C++ prefers designs in which an obligation is discharged automatically.

Consider this function: which allocates an integer, uses it, and should free it before returning, the release occurs at the end: allocation → use → release.

#include <iostream>

void process()
{
    int* ptr { new int{ 42 } };
    std::cout << *ptr << '\n';
    // Release the dynamically allocated object
    delete ptr;
}

Allocation, use, and release show up linearly, and the code appears correct. Yet any change that inserts an early exit prior to delete would render the function into a leak.

#include <iostream>

void process(bool earlyExit)
{
    // Allocate this object dynamically
    int* ptr { new int{ 42 } };
    std::cout << *ptr << '\n';

    if (earlyExit)
        return; // Memory leaked

    delete ptr;
}

When earlyExit becomes true, the function returns immediately without the statement delete being executed, leading to the allocated integer never being deallocated for the lifetime of the program. The compiler gives no warning; the error will manifest eventually, if enough leaks accumulate, when the program runs.

A second type of failure is caused by issuing two deletes on the same pointer.

#include <iostream>

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

    delete ptr;
    delete ptr; // Undefined behavior

    return 0;
}

The first delete frees memory and returns it to the available pool; the second operates on a pointer that your program no longer owns, and as a consequence causes behavior that is undefined, be it immediate crashing, memory corruption, or simply failure occurring much later upon some other allocation.

And the third is that of dereferencing a pointer whose memory has already been deleted.

#include <iostream>

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

    delete ptr;

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

    return 0;
}

When a memory address refers to memory previously pointed to by the commented line. In each case, the error could have resulted from minor, barely noticeable changes to the code.

This logic remains the same, but now is safe. Here the pointer is held by an object; the responsibility for freeing the memory and any other resource owned by the object, not shown for simplicity, resides within its destructor.

#include <iostream>

class IntOwner
{
public:
    IntOwner(int value)
        : ptr{ new int{ value } }
    {
    }

    // Run this destructor when the object lifetime ends
    ~IntOwner()
    {
        delete ptr;
    }

    int get() const
    {
        return *ptr;
    }

private:
    int* ptr;
};

void process(bool earlyExit)
{
    IntOwner owner{ 42 };
    std::cout << owner.get() << '\n';

    if (earlyExit)
        return; // Destructor still runs
}

When the process function returns, either via normal falloff or an early return, the object owner goes out of scope and its destructor is invoked, in which we deallocate the integer allocated in the constructor. The delete is always invoked at the same point, and it is thus never forgotten. Nor can two delete statements be written for the same pointer that would access the previously freed block.

No member of owner can be accessed after the delete in its destructor has executed, and no programmer can dereference an invalid pointer through owner once it has been destroyed.

Exercise: Why Modern C++ Avoids Manual Memory ManagementStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Why Modern C++ Avoids Manual Memory Management

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

Your challenge

Replace the raw owning pointer with std::make_unique<int>(99). Print its value in a nested scope, remove the manual delete, and explain the automatic cleanup at scope exit.

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 } };

    delete ptr;

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

    return 0;
}

Hints

Hint 1

This short program relies on remembering delete ptr explicitly. Merely ending the pointer variable's scope would not release the int created by new.

Hint 2

For a modern version, use std::make_unique<int>(99) with <memory> and let its owner clean up automatically. Do not retain the manual delete when ownership moves to a smart pointer.

Worked solution

Manual new/delete is replaced by make_unique. The smart pointer is the sole owner and releases the int automatically at the closing brace, including on exception unwinding. There is no manual delete to forget or duplicate.

#include <iostream>
#include <memory>
int main()
{
    {
        auto owner = std::make_unique<int>(99);
        std::cout << *owner << '\n';
    }
    std::cout << "Scope ended\n";
}

Example output

99
Scope ended

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.