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.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
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 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.
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.
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.
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.
Now you try. Use alignof and sizeof to ask the compiler directly about a type's alignment and a struct's padded size:
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.
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).
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.
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.