Skip to main content
    Courses/C++/Memory Management

    Lesson 13 • Expert

    Memory Management

    By the end of this lesson you'll know exactly where your data lives — stack or heap — how to allocate and free it by hand with new/delete, why leaks and dangling pointers happen, and how RAII and smart pointers let modern C++ manage memory for you so you almost never write delete again.

    What You'll Learn

    • Tell stack (automatic) from heap (dynamic) storage and pick the right one
    • Allocate and free with new/delete and new[]/delete[] correctly
    • Spot memory leaks, dangling pointers, double frees, and use-after-free
    • Use RAII so a destructor frees memory automatically
    • Default to unique_ptr + make_unique and move ownership instead of copying
    • Share with shared_ptr and break reference cycles with weak_ptr

    💡 Real-World Analogy

    Think of two ways to store your stuff. The stack is the desk in front of you: grabbing something is instant, and when you stand up everything is cleared away for you — but the desk is small. The heap is a self-storage warehouse: there's room for almost anything of any size, but you rent the unit and you must remember to return the key. Forget to return it and you keep paying for a unit you can't even reach — that's a memory leak. A smart pointer is a rental agreement that returns the key for you the moment you no longer need the unit.

    1. Stack vs Heap, Automatic vs Dynamic

    Every program has two regions of memory. The stack holds your ordinary local variables; this is automatic storage because each variable is created when you declare it and destroyed automatically when it goes out of scope. The heap is for dynamic storage — memory you request at runtime with new and keep until you hand it back with delete. The stack is fast but small (a few MB); the heap is huge but you manage its lifetime by hand.

    FeatureStack (automatic)Heap (dynamic)
    SpeedVery fastSlower
    SizeLimited (~1–8 MB)Large (GBs)
    LifetimeEnds at scope exitUntil you delete
    SizingFixed at compile timeChosen at runtime
    Allocateint x = 5;int* p = new int(5);

    Read this worked example, run it, and watch which values are cleaned up for you and which you free yourself.

    Worked example: stack vs heap

    See automatic stack storage next to manual heap storage.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    void stackExample() {
        int local = 10;          // STACK: automatic storage, freed at return
        int arr[3] = {1, 2, 3};  // STACK array: size fixed at compile time
        cout << "Stack value: " << local << "\n";
        // 'local' and 'arr' vanish automatically when this function ends
    }
    
    void heapExample() {
        int* p = new int(99);    // HEAP: dynamic storage, YOU own its lifetime
        cout << "Heap value: " << *p << "\n";  // *p reads the pointed-to int 
    ...

    2. Allocating by Hand: new and delete

    new reserves heap memory and returns a pointer to it; delete hands that memory back. For a single value you pair new with delete; for an array you pair new[] with delete[]. The forms are not interchangeable — using delete on something from new[] is undefined behaviour. The golden rule: every new has exactly one matching delete, every new[] exactly one delete[].

    Worked example: new / delete and new[] / delete[]

    Allocate and free a single int and a dynamic array.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    int main() {
        // 1) Single value: new returns a pointer to one fresh int on the heap.
        int* ptr = new int(42);          // allocate + initialise to 42
        cout << "Value:   " << *ptr << "\n";   // *ptr  -> 42  (dereference)
        *ptr = 100;                      // change the value through the pointer
        cout << "Updated: " << *ptr << "\n";   // *ptr  -> 100
    
        delete ptr;                      // free the single int
        ptr = nullptr;             
    ...

    Your turn. Fill in the three blanks to allocate one double, free it, and nullify the pointer.

    🎯 Your turn: allocate and free one value

    Fill in the ___ blanks, then check your output against the expected lines.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    int main() {
        // 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
    
        // 1) Allocate ONE double on the heap, initialised to 3.14
        double* pi = ___;        // 👉 new double(3.14)
    
        cout << "pi = " << *pi << "\n";   // *pi reads the value through the pointer
    
        // 2) Free that single double (single value -> plain delete)
        ___;                     // 👉 delete pi;
    
        // 3) Nullify the pointer so it can't dangle
        pi = ___;   
    ...

    Now do it for an array. Remember the array form of both new and delete.

    🎯 Your turn: a runtime-sized array

    Allocate with new[], then free with delete[].

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    int main() {
        // 🎯 YOUR TURN — a runtime-sized array on the heap.
        int n = 3;
    
        // 1) Allocate an array of n ints on the heap
        int* scores = ___;       // 👉 new int[n]
    
        scores[0] = 70;
        scores[1] = 85;
        scores[2] = 95;
    
        int total = 0;
        for (int i = 0; i < n; i++) total += scores[i];
        cout << "Average: " << total / n << "\n";
    
        // 2) Free the ARRAY (array form, not plain delete)
        ___;                     // 👉 delet
    ...

    3. Leaks, Dangling Pointers & Double Frees

    Manual memory has three classic traps. A memory leak is new with no matching delete — the memory stays reserved but unreachable, like losing your locker key. A dangling pointer still holds an address you've already freed; reading through it is undefined behaviour (it might print garbage, might crash, might silently corrupt data). A double free is calling delete twice on the same block, which crashes. The simple discipline: set a raw pointer to nullptr right after delete — deleting nullptr is a safe no-op.

    Worked example: the three classic traps

    Watch how leaks, dangling, and double frees arise — and the nullptr fix.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // LEAK: allocates but never frees -> 400 bytes lost on every call
    void leaky() {
        int* data = new int[100];
        // ... no delete[] here -> memory is reserved but unreachable forever
    }
    
    int main() {
        // --- Dangling pointer: using memory after it's freed ---
        int* p = new int(42);
        cout << "Before delete: " << *p << "\n";   // Before delete: 42
        delete p;                 // memory is returned to the system
        // cout << *p;            // 
    ...

    4. RAII: Let a Destructor Do the Cleanup

    Remembering to delete on every path — including when an exception is thrown — is hard, and humans forget. RAII (Resource Acquisition Is Initialisation) is the C++ answer: wrap the resource in an object whose constructor acquires it and whose destructor releases it. Because C++ guarantees the destructor runs the moment the object leaves scope, cleanup becomes automatic and leak-proof. This single idea is the foundation everything else in modern C++ builds on.

    Worked example: RAII wrapper

    The destructor frees the heap memory automatically at scope exit.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // RAII = Resource Acquisition Is Initialisation.
    // The constructor grabs the resource; the destructor releases it.
    // When the object dies, cleanup happens AUTOMATICALLY — even on exceptions.
    class IntBuffer {
        int* data;
        int size;
    public:
        IntBuffer(int n) : data(new int[n]()), size(n) {   // () zero-initialises
            cout << "Allocated " << n << " ints\n";
        }
        ~IntBuffer() {                 // destructor runs at end of scope
           
    ...

    5. Smart Pointers — the Modern Default

    You rarely write your own RAII wrapper because the standard library already ships them: smart pointers, in <memory>. They look and act like raw pointers (use * and ->) but they own what they point at and free it automatically. They also encode ownership semantics — who is responsible for the memory — right in the type.

    unique_ptr is the sole owner: build it with make_unique, and it frees the object at scope exit. You can't copy a unique_ptr (that would mean two owners) but you can move ownership with std::move. shared_ptr allows shared ownership through a reference count: each copy bumps the count, each destruction lowers it, and the object is freed only when the count hits zero. Default to unique_ptr; use shared_ptr only when ownership is truly shared.

    Worked example: unique_ptr & shared_ptr

    Move unique ownership; watch shared_ptr's reference count rise and fall.

    Try it Yourself »
    C++
    #include <iostream>
    #include <memory>      // smart pointers live here
    using namespace std;
    
    struct Widget {
        int id;
        Widget(int i) : id(i) { cout << "Widget " << id << " built\n"; }
        ~Widget()            { cout << "Widget " << id << " destroyed\n"; }
    };
    
    int main() {
        // unique_ptr: SOLE owner. Frees automatically. No new/delete in sight.
        unique_ptr<Widget> a = make_unique<Widget>(1);   // Widget 1 built
        cout << "a owns Widget " << a->id << "\n";        // arrow works like 
    ...

    6. weak_ptr: Breaking Reference Cycles

    shared_ptr has one trap. If object A holds a shared_ptr to B and B holds one back to A, each keeps the other's reference count above zero forever — neither is ever freed. That's a reference cycle, and it leaks. A weak_ptr fixes it: it observes an object without owning it, so it doesn't raise the reference count. To use what it points at, you call .lock(), which gives you a temporary shared_ptr (empty if the object is already gone). Rule of thumb: make the "back-reference" the weak one.

    Worked example: weak_ptr breaks the cycle

    A links to B strongly; B links back weakly, so both free cleanly.

    Try it Yourself »
    C++
    #include <iostream>
    #include <memory>
    using namespace std;
    
    struct Node {
        int value;
        shared_ptr<Node> next;     // strong: keeps 'next' alive
        weak_ptr<Node>   prev;     // weak: observes without owning -> no cycle
        Node(int v) : value(v) { cout << "Node " << v << " built\n"; }
        ~Node()                { cout << "Node " << value << " destroyed\n"; }
    };
    
    int main() {
        auto a = make_shared<Node>(1);
        auto b = make_shared<Node>(2);
    
        a->next = b;     // a strongly owns b
      
    ...

    Common Errors (and the fix)

    • Memory leak — forgot to free: int* p = new int[100]; with no delete[] p;. The memory is reserved forever. Fix: match every new with a delete, or use a smart pointer so you can't forget.
    • Double free / "free(): double free detected": calling delete p; twice on the same pointer crashes. Fix: set p = nullptr; after the first delete — deleting nullptr does nothing.
    • Use-after-free (dangling): reading *p after delete p; is undefined behaviour — garbage, a crash, or silent corruption. Fix: nullify after delete, or let a smart pointer manage lifetime.
    • Wrong delete form: using delete p; on memory from new int[n] (or delete[] on a single new) is undefined behaviour. Fix: new[] pairs only with delete[]; new only with delete.
    • shared_ptr cycle leak: two shared_ptrs pointing at each other never reach refcount 0, so neither frees. Fix: make one direction a weak_ptr to break the cycle.
    • "error: use of deleted function" copying unique_ptr: auto b = a; where a is a unique_ptr won't compile. Fix: move it — auto b = std::move(a);.

    Pro Tips

    • 💡 Prefer the stack: if the size is known and small, a plain local beats new every time — no leaks possible.
    • 💡 Default to unique_ptr: it has zero overhead over a raw pointer and clearly states single ownership.
    • 💡 Reach for make_unique/make_shared: they're exception-safe and you never write a naked new.
    • 💡 Detect leaks with tools: run valgrind ./program or build with -fsanitize=address to catch leaks and use-after-free.

    📋 Quick Reference

    TaskCode
    Allocate one valueint* p = new int(5);
    Free one valuedelete p; p = nullptr;
    Allocate an arrayint* a = new int[n];
    Free an arraydelete[] a;
    Sole owner (modern)auto u = make_unique<T>(args);
    Move ownershipauto v = std::move(u);
    Shared ownerauto s = make_shared<T>(args);
    Non-owning observerweak_ptr<T> w = s; w.lock();

    Frequently Asked Questions

    Q: When should I use the heap (new) instead of the stack?

    Reach for the heap only when you genuinely need it: the size isn't known until runtime, the object must outlive the function that created it, or it's too big for the stack (a few MB). For everything else, prefer plain stack variables — they're faster and clean themselves up.

    Q: What is the difference between unique_ptr and shared_ptr?

    A unique_ptr is the sole owner of its object and frees it when it goes out of scope — ownership can be moved but never copied. A shared_ptr lets several pointers co-own one object; it keeps a reference count and frees the object only when the last shared_ptr is destroyed. Use unique_ptr by default and shared_ptr only when ownership is genuinely shared.

    Q: Why does shared_ptr leak when two objects point at each other?

    Two shared_ptrs pointing at each other form a cycle: each keeps the other's reference count at 1, so neither ever reaches zero and neither is freed. Break the cycle by making one direction a weak_ptr, which observes the object without bumping the reference count.

    Q: Do I still need to write new and delete in modern C++?

    Almost never. Modern C++ (C++11 and later) replaces raw new/delete with make_unique and make_shared, which own the memory and free it automatically. You learn new/delete to understand what smart pointers do under the hood and to read older code, but you write smart pointers.

    Q: What is a dangling pointer and how do I avoid one?

    A dangling pointer still holds the address of memory that has already been freed (or of a local variable that went out of scope). Reading or writing through it is undefined behaviour. Avoid it by setting raw pointers to nullptr right after delete, never returning the address of a local, and using smart pointers so lifetime is managed for you.

    Mini-Challenge: Own an Account with a Smart Pointer

    No blanks this time — just a brief and an outline. Create an Account on the heap using make_unique, update it through the -> arrow, and notice you never write delete. Run it and check your output against the expected line.

    🎯 Mini-Challenge: make_unique in action

    Build, mutate, and print — letting unique_ptr free the memory for you.

    Try it Yourself »
    C++
    #include <iostream>
    #include <memory>
    using namespace std;
    
    struct Account {
        string owner;
        double balance;
        Account(string o, double b) : owner(o), balance(b) {}
    };
    
    int main() {
        // 🎯 MINI-CHALLENGE: own an Account with a smart pointer
        // 1. Use make_unique<Account>(...) to create an Account
        //    for "Sam" with a balance of 100.0  (store it in a unique_ptr)
        // 2. Use the -> arrow to add 50 to the balance
        // 3. Print: "Sam now has 150"
        // 4. Notice: you write 
    ...

    🎉 Lesson Complete

    • ✅ Stack = automatic storage (freed at scope exit); heap = dynamic storage (freed by you)
    • new/delete for one value, new[]/delete[] for arrays — never mix the forms
    • ✅ Leaks, dangling pointers, and double frees come from manual delete; nullify after freeing
    • ✅ RAII ties cleanup to a destructor so it always runs, even on exceptions
    • unique_ptr + make_unique is the default; move ownership instead of copying
    • shared_ptr counts references; weak_ptr observes without owning to break cycles
    • Next lesson: Move Semantics & Smart Pointers — go deeper on moves and ownership transfer

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service