Modern C++ Memory Model

Reviewed & published by Brayan K

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.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 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.

#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 << "global (static): " << globalCounter << endl;  // 42
    cout << "local  (stack):  " << onStack << endl;        // 10
    cout << "dynamic (heap):  " << *onHeap << endl;        // 99
    cout << "literal (data):  " << literal << endl;        // hello

    delete onHeap;   // YOU must free heap memory — the stack frees itself
    return 0;
}

// ✅ Expected output:
//    global (static): 42
//    local  (stack):  10
//    dynamic (heap):  99
//    literal (data):  hello

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.

#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 next line
    delete p;                   // ...until YOU delete it -> destroy 3

    cout << "--- leave main ---" << endl;
    return 0;
    // Expected order:
    //   build 1, build 2, destroy 2, destroy 1,
    //   build 3, destroy 3, leave main
}

// ✅ Expected output:
//    --- enter main ---
//    build 1
//    build 2
//    destroy 2
//    destroy 1
//    build 3
//    destroy 3
//    --- leave main ---

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.

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

    // ✅ Expected output:
    //    Note created
    //    using the note...
    //    Note destroyed
    //    done
    return 0;
}

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.

#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 byte
    char  d;   // 1 byte ... + 2 padding -> 8 bytes (better!)
};

int main() {
    cout << "alignof(int):    " << alignof(int) << endl;    // 4
    cout << "alignof(double): " << alignof(double) << endl; // 8
    cout << "sizeof(Padded):  " << sizeof(Padded) << endl;  // 12
    cout << "sizeof(Packed):  " << sizeof(Packed) << endl;  // 8

    // alignas forces a stricter alignment (e.g. a cache line):
    alignas(16) int aligned = 5;
    cout << "aligned value:   " << aligned << endl;         // 5
    return 0;
}

// ✅ Expected output:
//    alignof(int):    4
//    alignof(double): 8
//    sizeof(Padded):  12
//    sizeof(Packed):  8
//    aligned value:   5

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

#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)

    // ✅ Expected output:
    //    align double: 8
    //    size  Pixel:  16
    return 0;
}

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.

#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=" << b << endl;  // a=5 b=50

    // HAPPENS-BEFORE across threads: the atomic store 'publishes' the
    // write to payload; the atomic load 'sees' it, so the order is safe.
    thread writer([]{
        payload = 99;              // 1) write the data
        ready.store(true);         // 2) publish: happens-before the load
    });
    thread reader([]{
        while (!ready.load()) {}   // 3) wait until published
        cout << "payload = " << payload << endl;  // 4) safely sees 99
    });
    writer.join();
    reader.join();
    return 0;
}

// ⚠️ No expected-output panel for this one, on purpose.
// Threads interleave differently on every run and on every machine, so
// there is no output to promise.
//
// One real run on the machine that builds this site printed:
//    a=5 b=50
//    payload = 99

🔎 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

Common Errors (and the fix)

📋 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

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.

#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 it so its destructor runs.
    // 4. Print "finished" at the very end.
    //
    // ✅ Expected output:
    //    open 1
    //    open 2
    //    close 2
    //    close 1
    //    open 3
    //    close 3
    //    finished

    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

Into which four segments is a program's memory commonly divided?

  • input, output, cache, registers
  • L1, L2, L3, RAM
  • code/text, static/data, stack, heap
  • read, write, execute, swap

Answer: code/text, static/data, stack, heap. Memory splits into code/text (machine code), static/data (globals, literals), stack (locals), and heap (new allocations).

Where does a local variable created with 'int onStack = 10;' live?

  • The stack (automatic storage)
  • The heap
  • The data segment
  • The code segment

Answer: The stack (automatic storage). A plain local goes on the stack with automatic storage — it is freed automatically when its scope ends.

Which are the four storage durations in C++?

  • short, int, long, double
  • public, private, protected, friend
  • const, volatile, mutable, register
  • automatic, static, dynamic, thread

Answer: automatic, static, dynamic, thread. The four storage durations are automatic (locals), static (globals/static locals), dynamic (new), and thread (thread_local).

In what order are automatic (stack) objects in a block destroyed?

  • The same order they were created
  • Reverse order of creation
  • Random order
  • Alphabetical order

Answer: Reverse order of creation. Stack objects are destroyed in reverse order of creation at the end of their block, so 'build 1, build 2' destroys as 'destroy 2, destroy 1'.

Why can sizeof(struct) be larger than the sum of its members?

  • The compiler inserts padding bytes so each member meets its alignment requirement
  • The compiler adds debug info
  • Members are stored twice
  • sizeof counts the type name

Answer: The compiler inserts padding bytes so each member meets its alignment requirement. Each type must start on an aligned address, so the compiler inserts padding between members. Reordering members largest-to-smallest often removes it.

What does alignof(double) typically return on a common 64-bit platform?

  • 1
  • 4
  • 8
  • 16

Answer: 8. A double usually wants an address divisible by 8, so alignof(double) is 8 (and alignof(int) is 4).

What does alignas(16) do to a variable?

  • Sets its size to 16
  • Forces it to start on a 16-byte-aligned address (a stricter alignment)
  • Pads it to 16 members
  • Makes it const

Answer: Forces it to start on a 16-byte-aligned address (a stricter alignment). alignas(N) demands a stricter alignment — e.g. alignas(16) places the variable on a 16-byte boundary, useful for cache lines or SIMD.

What does the as-if rule allow the compiler to do?

  • Change your program's output
  • Ignore your code entirely
  • Add new features
  • Reorder, combine, or delete operations as long as observable behaviour (I/O, volatile) is unchanged

Answer: Reorder, combine, or delete operations as long as observable behaviour (I/O, volatile) is unchanged. The as-if rule lets the optimizer transform code freely as long as observable behaviour stays the same — that's how optimised builds get fast.

What does a 'happens-before' relationship via std::atomic guarantee across threads?

  • Threads run in order
  • Writes done before an atomic store are visible to a thread that reads via the matching atomic load
  • Only one thread runs
  • Atomics are slower

Answer: Writes done before an atomic store are visible to a thread that reads via the matching atomic load. When one thread publishes via an atomic store and another sees it via an atomic load, every write before the store is guaranteed visible after the load.

What typically causes a stack overflow?

  • Too many globals
  • Using std::vector
  • Deep or infinite recursion running past the stack's limit (~1-8 MB)
  • Forgetting delete

Answer: Deep or infinite recursion running past the stack's limit (~1-8 MB). The stack is small. Deep or infinite recursion (or huge local arrays) runs past its limit, producing a segmentation fault. Move large data to the heap.

Continue this course

Frequently asked questions

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.

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.

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.

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.

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.