Skip to main content
    Courses/C++/Modern C++ Memory Model

    Lesson 15 • Advanced

    Modern C++ Memory Model

    By the end of this lesson you'll know where every variable lives in memory, how long it stays alive, why structs carry hidden padding, what the compiler is allowed to rearrange behind your back, and the first idea behind safe multithreading — the mental model that separates someone who writes C++ from someone who truly understands it.

    What You'll Learn

    • Map a program's memory into four segments: code, static/data, stack, heap
    • Tell apart the four storage durations: automatic, static, dynamic, thread
    • Reason about object lifetime — when constructors and destructors fire
    • Explain alignment and padding, and measure them with alignof / sizeof
    • Use alignas to force a stricter alignment when you need it
    • Understand the as-if rule and the happens-before idea behind thread safety

    💡 Real-World Analogy

    Think of your program's memory as a building. The code segment is the printed instruction manual bolted to the wall — fixed, read-only. The static/data segment is the lobby noticeboard: a few items that stay pinned up the entire time the building is open. The stack is a tall stack of trays in the canteen — you can only add or remove from the top, and trays vanish in the exact reverse order you added them. The heap is a giant warehouse: you can request a shelf of any size at any time, but you must remember to hand the shelf back, or it stays reserved forever (a memory leak). Almost everything in this lesson is just "which part of the building does this object live in, and who clears it away?"

    📊 The Four Memory Segments

    SegmentHoldsWho clears itGrows
    code / textCompiled machine codeOS at exitFixed
    static / dataGlobals, statics, literalsAfter main()Fixed
    stackLocals, function callsAutomatically (LIFO)↓ down
    heapnew allocationsYou (delete)↑ up

    The stack and heap grow toward each other. A stack overflow happens when the stack runs past its limit (often 1–8 MB) — deep or infinite recursion is the usual culprit.

    1. Where Variables Live: The Memory Layout

    When your program runs, the operating system hands it an address space split into the segments above. Where a variable lives is decided by how you create it: a plain local goes on the stack, a global or static goes in the static/data segment, and anything you create with new goes on the heap. The compiled instructions themselves sit in the read-only code segment. Read this worked example, run it, then check the output against the comments.

    Worked example: the four segments

    See a global, a local, a heap allocation, and a literal — each in its own segment.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // Lives in the STATIC/DATA segment — exists for the whole program.
    int globalCounter = 42;
    
    int main() {
        // A local — AUTOMATIC storage on the STACK.
        int onStack = 10;
    
        // Allocated with 'new' — DYNAMIC storage on the HEAP.
        int* onHeap = new int(99);
    
        // A string literal — read-only DATA segment (and the CODE/text
        // segment holds the compiled machine code that runs all this).
        const char* literal = "hello";
    
        cout << "glob
    ...

    2. Storage Duration & Object Lifetime

    Storage duration answers "how long does this object's memory stay alive?" C++ has four kinds. Automatic is the default for locals — the object is born when control reaches it and dies at the end of its { } block, in reverse order of creation. Static (globals and static locals) lasts the whole program. Dynamic (made with new) lasts until you call delete. Thread (thread_local) gives each thread its own copy. The example below makes the constructor and destructor announce themselves so you can watch lifetimes unfold.

    Worked example: automatic vs dynamic lifetime

    Watch construction and destruction order for stack and heap objects.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    struct Beep {
        int id;
        Beep(int i) : id(i) { cout << "build " << id << endl; }
        ~Beep()             { cout << "destroy " << id << endl; }
    };
    
    int main() {
        cout << "--- enter main ---" << endl;
        {
            Beep a(1);              // AUTOMATIC: built here...
            Beep b(2);              // ...and here
        }   // <- block ends: destroyed in REVERSE order -> destroy 2, destroy 1
    
        Beep* p = new Beep(3);      // DYNAMIC: lives past the n
    ...

    Your turn. A heap object only dies when you free it — forget the delete and its destructor never runs (a leak). Fill in the blank so the Note is cleaned up properly.

    🎯 Your turn: free the heap object

    Replace the ___ so the destructor runs and there's no leak.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    struct Note {
        Note()  { cout << "Note created" << endl; }
        ~Note() { cout << "Note destroyed" << endl; }
    };
    
    int main() {
        // 🎯 YOUR TURN — this object is on the HEAP, so YOU control its life.
    
        Note* n = new Note();    // dynamic storage duration
    
        cout << "using the note..." << endl;
    
        // 1) Free the heap object so its destructor runs (no leak!)
        ___;                     // 👉 delete n;
    
        cout << "done" << endl;
    
        // ✅ Expe
    ...

    3. Alignment & Padding

    The CPU reads memory fastest when a value sits on an address that's a multiple of its size — this requirement is called alignment. An int usually wants an address divisible by 4, a double one divisible by 8. To honour that inside a struct, the compiler quietly inserts padding bytes between members, which is why sizeof a struct can be bigger than the sum of its parts. You can measure alignment with alignof(T), and demand a stricter one with alignas(N). Reordering members largest-to-smallest often removes padding.

    Worked example: alignof, sizeof & padding

    See how member order changes a struct's size, and force alignment with alignas.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    // Each type wants to START on an address that is a multiple of its
    // alignment. The compiler inserts PADDING bytes to make that happen.
    struct Padded {
        char  c;   // 1 byte  ... then 3 padding bytes so the int aligns
        int   i;   // 4 bytes (must start on a multiple of 4)
        char  d;   // 1 byte  ... then 3 padding bytes to round the struct
    };            // total: 12 bytes, not 6!
    
    struct Packed {
        int   i;   // 4 bytes
        char  c;   // 1 by
    ...

    Now you try. Use alignof and sizeof to ask the compiler directly about a type's alignment and a struct's padded size:

    🎯 Your turn: measure alignment & size

    Fill in the alignof and sizeof calls, then check the numbers.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    struct Pixel {
        double x;   // 8 bytes (alignment 8)
        char   tag; // 1 byte  ... + 7 padding -> struct is 16 bytes
    };
    
    int main() {
        // 🎯 YOUR TURN — ask the compiler about alignment & size.
    
        // 1) Print the alignment requirement of a double
        cout << "align double: " << ___ << endl;   // 👉 alignof(double)
    
        // 2) Print the total size of Pixel (includes padding)
        cout << "size  Pixel:  " << ___ << endl;    // 👉 sizeof(Pixel)
    
        
    ...

    4. The As-If Rule & Happens-Before

    The compiler is not obliged to run your statements literally. The as-if rule lets it reorder, combine, or delete operations as long as the program's observable behaviour (its I/O and volatile access) is unchanged — that's how optimised builds get fast. On a single thread you never notice. Across threads, though, reordering can be dangerous, so C++ gives you the happens-before relationship: when a thread publishes data through a std::atomic store and another thread sees it through an atomic load, every write before the store is guaranteed visible after the load. That ordering guarantee is the whole foundation of safe multithreading.

    Worked example: as-if rule + happens-before

    See single-thread reordering, then a safe cross-thread publish with std::atomic.

    Try it Yourself »
    C++
    #include <iostream>
    #include <atomic>
    #include <thread>
    using namespace std;
    
    atomic<bool> ready{false};   // atomic = safe to touch from many threads
    int payload = 0;             // plain int, guarded by 'ready'
    
    int main() {
        // The AS-IF RULE: the compiler may reorder these two lines because,
        // on a single thread, the observable result is identical:
        int a = 2 + 3;   // the compiler is free to just write 5
        int b = a * 10;  // ...and fold this too
        cout << "a=" << a << " b=" 
    ...

    🔎 Deep Dive: stack vs heap, in one breath

    The stack is automatic and fast: allocation is just moving a pointer, and cleanup is free because the object dies with its scope. The catch is size — the stack is small, so huge arrays and deep recursion overflow it. The heap is flexible: any size, any lifetime, but every new needs a matching delete, and forgetting leaks memory. The modern answer is to almost never write raw new/delete yourself — let a std::unique_ptr or container own the heap memory so the destructor frees it for you (RAII).

    int x = 5;                 // stack: freed automatically at scope end
    int* p = new int(5);       // heap:  YOU must delete p; or it leaks
    auto up = make_unique<int>(5); // heap, but freed for you (RAII) ✅

    Pro Tips

    • 💡 Prefer the stack: if an object can live on the stack, put it there — it's faster and cleans itself up.
    • 💡 Let RAII own the heap: wrap new in a unique_ptr or container so you never hand-write delete.
    • 💡 Order members big-to-small: placing larger types first often removes padding and shrinks a struct.
    • 💡 Default to safe ordering: use a mutex or default atomic first; reach for relaxed/acquire/release only once you've measured a need.

    Common Errors (and the fix)

    • Memory leak: you called new but never delete, so the destructor never runs and the heap shelf is held forever. Match every new with a delete — or better, use a unique_ptr so it's freed automatically.
    • Dangling pointer (returning a local's address): a local lives on the stack and dies at the end of its block, so a pointer to it points at freed memory. Return by value, or allocate on the heap and pass ownership out via a smart pointer.
    • Stack overflow: "Segmentation fault" from deep or infinite recursion means the stack ran past its limit. Add a base case, or move large data to the heap.
    • "struct is bigger than I expected": hidden padding inflated sizeof. Reorder members largest-to-smallest, and remember the size includes alignment padding.
    • Data race: two threads touch the same plain variable with no synchronisation, so there's no happens-before — the result is undefined. Guard it with a std::mutex or make it std::atomic.

    📋 Quick Reference

    ConceptCode / DetailResult
    Stack (automatic)int x = 5;freed at scope end
    Heap (dynamic)int* p = new int(5);you delete p;
    Static durationstatic int n = 0;whole program
    Alignmentalignof(double)8
    Force alignmentalignas(16) int a;16-byte aligned
    Padded sizesizeof(MyStruct)≥ sum of members
    Cross-thread publishflag.store(true)happens-before

    Frequently Asked Questions

    Q: What is the difference between the stack and the heap?

    The stack is a fast region the compiler manages for you: every local variable is created on it automatically and destroyed automatically when its block ends. The heap is a larger region you manage by hand (or via smart pointers) with new/delete, where objects live until you free them. Stack allocation is cheap and automatic; heap allocation is flexible but you are responsible for the object's lifetime.

    Q: What does 'storage duration' actually mean?

    Storage duration is how long the memory for an object stays alive. C++ has four kinds: automatic (a local — lives until its block ends), static (a global or static local — lives for the whole program), dynamic (created with new — lives until you delete it), and thread (a thread_local — one copy per thread). The duration decides when the object is created and destroyed.

    Q: Why is there padding between my struct members?

    Each type has an alignment requirement — an address it must start on (an int usually wants a multiple of 4, a double a multiple of 8). The compiler inserts padding bytes so every member lands on a valid address, which is why sizeof(struct) can be larger than the sum of its members. Reordering members from largest to smallest often removes padding and shrinks the struct.

    Q: What is the as-if rule?

    The as-if rule lets the compiler transform your code in any way it likes — reorder, combine, or delete operations — as long as the program's observable behaviour stays the same. Observable means I/O and access to volatile data. It is why optimised builds can be far faster than the literal instructions you wrote, while still producing the same output.

    Q: Do I need to learn memory ordering to write threads?

    Not at first. For everyday threading, protect shared data with a std::mutex or use std::atomic with its default (sequentially consistent) ordering, and the happens-before guarantees are handled for you. The relaxed/acquire/release orderings are an expert tool for squeezing out performance once you understand the model — reach for them last, not first.

    Mini-Challenge: Lifetime Detective

    No blanks this time — just a brief and an outline. Predict the destruction order before you run it, then build it and check your output against the comments. Getting this right means you've internalised how lifetimes actually work.

    🎯 Mini-Challenge: trace the lifetimes

    Create stack and heap objects and predict the construct/destroy order.

    Try it Yourself »
    C++
    #include <iostream>
    using namespace std;
    
    struct Tracker {
        int id;
        Tracker(int i) : id(i) { cout << "open " << id << endl; }
        ~Tracker()             { cout << "close " << id << endl; }
    };
    
    int main() {
        // 🎯 MINI-CHALLENGE: lifetime detective
        // 1. In a { } block, create two stack Trackers: Tracker(1), Tracker(2).
        //    Predict the destruction order BEFORE you run it (hint: reverse).
        // 2. After the block, create one heap Tracker with new Tracker(3).
        // 3. delete i
    ...

    🎉 Lesson Complete

    • ✅ Memory splits into four segments: code, static/data, stack, heap
    • ✅ Four storage durations: automatic, static, dynamic, thread
    • ✅ Stack objects die in reverse order at scope end; heap objects die when you delete them
    • alignof measures alignment, sizeof includes padding, alignas forces a stricter alignment
    • ✅ The as-if rule lets the compiler rearrange code that has the same observable result
    • Happens-before via std::atomic makes one thread's writes safely visible to another
    • Next lesson: Advanced OOP — virtual tables, polymorphic dispatch, and inheritance

    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

    Install LearnCodingFast

    Learn faster with the app on your home screen.