Every variable in a C++ program lives in one of a few memory regions. The two that come up most often are the stack and the heap. They have different lifetimes, different costs, and different rules for what can go wrong. This lesson covers what each region is, how a running program lays out its memory, how function calls use stack frames, when to pick one over the other, and what happens when the stack runs out.
When the operating system loads a C++ program into memory, it gives the process a single block of virtual address space and carves it into named regions. Each region has a purpose and a set of rules.
The diagram shows the typical layout on a Linux or macOS process. Low addresses are at the top, high addresses at the bottom. The five regions:
static variables that were initialized with a non-zero value at compile time (e.g., int taxRate = 7; at file scope).int orderCount; reads as 0 instead of garbage.new (or malloc in C code) and release with delete. It grows upward toward higher addresses.The stack and heap grow toward each other through the unused address space in between. On modern 64-bit systems that space is enormous, so collisions are rare, but the stack still has a hard size limit set by the OS.
A small program that prints the addresses of variables in each region:
Sample Output (g++ on Linux):
The exact addresses differ on every run (address space layout randomization moves them around), but the relative ordering is consistent. Code lives lowest, then data and BSS, then heap (a few address bits higher), then a huge gap, then the stack at very high addresses.
Quick Check: Where does the integer 7 end up in memory?
<details> <summary>Answer</summary>
B. new int(7) allocates space on the heap and stores 7 there. The pointer p itself is a local variable on the stack, but the integer it points to lives on the heap.
</details>
The stack is a region of memory that follows a strict last-in-first-out discipline. Every time a function is called, a new chunk of stack memory called a stack frame is pushed onto the top. When the function returns, that frame is popped off and the memory is immediately reusable.
A stack frame holds three kinds of data:
The compiler knows the exact size of each frame at compile time, so allocating and freeing one is a single CPU instruction: adjust the stack pointer register by the frame size. No searching, no metadata to update, no locks.
When main calls cartTotal(24.99, 3), the runtime pushes a new frame on top of main's frame. That frame contains the parameters price and quantity, the locals subtotal and tax, and a saved return address pointing back into main. When cartTotal returns, the frame is popped, all four variables vanish, and execution resumes in main with total now holding the result.
The LIFO ordering is what makes recursion work. Each recursive call pushes a fresh frame with its own copy of every parameter and local. The frames stack up like plates, and they come off in reverse order as each call returns.
The middle column shows the stack at its peak: two frames stacked, cartTotal on top of main. As soon as the function returns, the orange frame is gone, and the memory it used is available again for the next call.
A few properties follow from how the stack works:
delete.The heap is the opposite of the stack in almost every way. It's larger, slower, and you control the lifetime of every allocation.
Two situations call for the heap:
The basic operations are new and delete:
new Product(...) asks the standard library's allocator for enough memory to hold one Product, runs the constructor in that memory, and returns a pointer to the new object. The object lives on the heap until delete gift; runs. The pointer gift itself is a local variable on the stack, so it disappears when main returns, but the heap object would survive forever if the delete were missing. That's a memory leak.
The heap is managed by a runtime allocator (typically the system malloc underneath new), which has to track which regions are in use and which are free. Every new involves searching a free-list or similar structure, possibly acquiring a lock if multiple threads are allocating, and updating metadata. A heap allocation is commonly 10 to 100 times slower than a stack allocation. The cost is usually invisible for a few news, but a tight loop that allocates thousands of small objects can become a bottleneck.
new needs a matching delete. Forget one and you leak. Call delete twice and you get undefined behavior. Modern C++ uses smart pointers (std::unique_ptr, std::shared_ptr) to automate this.new, and it may have to lock for thread safety.new and delete of different-sized objects can leave the heap full of small free gaps that are individually too small for new requests. The total free memory looks fine, but a large new fails. Stack memory never fragments because frames come and go in strict LIFO order.Quick Check: Which statement about new and delete is correct?
delete on a nullptr crashes the program.delete twice on the same non-null pointer is undefined behavior.delete p;, reading *p returns zero.<details> <summary>Answer</summary>
B. The standard guarantees delete nullptr; is a no-op (safe), but double-deleting a non-null pointer is undefined behavior. Reading through a pointer after delete is also undefined behavior, since the memory now belongs to the allocator.
</details>
A function call is more than "jump to the function and run it." The CPU has to remember where to come back to, the function needs space for its variables, and the arguments have to get from the caller to the callee. The stack handles all of this.
Consider a chain of calls:
When the program starts, main's frame is the only one on the stack. The call to finalPrice(200) pushes a new frame on top, holding the parameter basePrice = 200, the local afterDiscount, and the return address pointing back into main. Then applyDiscount(200, 10) is called, pushing another frame holding price = 200, percent = 10, the local discount, and the return address pointing back into finalPrice.
Three frames are on the stack at the deepest point. When applyDiscount returns 180, its frame is popped and the return address brings execution back into finalPrice. finalPrice then returns 180 and its frame is popped too. By the time main prints, only main's frame is left. The stack has gone up and back down in a perfect mirror of the call sequence.
This is why local variables can never escape their function safely. The frame they live in is gone the instant the function returns.
What is wrong with this code?
quantity lives in makeQuantity's stack frame. When the function returns, that frame is popped and the memory is up for grabs. The returned pointer is dangling, and reading *p is undefined behavior. The program might appear to print 50 (the bits might still be there), or it might print garbage, or it might crash.
Fix: put the value on the heap and return a pointer to it, or return the value:
For larger objects, return by value too. The compiler optimizes the return (return value optimization), so there's usually no actual copy.
Most variables should live on the stack. It's faster, automatic, and impossible to leak. Use the heap only when you need something the stack can't give you.
| Situation | Stack | Heap |
|---|---|---|
| Small local variable | Yes | No |
| Object outlives the function | No | Yes |
| Size known at compile time | Yes | Either |
| Size only known at runtime | No (use std::vector) | Yes |
Polymorphic ownership (Base* pointing to derived) | No | Yes |
| Very large object (multiple MB) | No (would overflow) | Yes |
| Default choice | Yes | No |
The "size only known at runtime" row needs one clarification. Raw arrays allocated with new[] are rarely the right approach in modern C++. std::vector puts its backing storage on the heap internally but gives you a stack-allocated handle. The storage can grow at runtime, and the cleanup is automatic when the std::vector goes out of scope.
The std::vector<double> prices(itemCount); line allocates itemCount doubles on the heap, but the prices handle is on the stack. When main returns, the vector's destructor automatically releases the heap memory. No delete to remember. This is the standard modern C++ pattern: pair heap storage with stack-managed ownership.
Quick Check: Which of these should live on the heap?
aliceCustomer bob points atall<details> <summary>Answer</summary>
B. Only the object bob points at is on the heap. alice is a stack-allocated Customer. The vector all is itself a stack variable, although its 100-element backing array lives on the heap (managed by the vector internally).
</details>
Each thread starts with a fixed-size stack, and going past the end is fatal. The OS detects the access to the page just below the stack and kills the program. This is stack overflow.
Two common causes:
Unbounded or too-deep recursion. Each call pushes a frame. A recursive function that doesn't have a proper base case will push frames until the stack is full.
This program crashes with a segmentation fault (Linux) or stack overflow exception (Windows) within milliseconds. The exact depth at which it crashes depends on frame size and stack limit, but it's typically tens of thousands of calls for tiny frames, fewer for larger ones.
Huge local arrays or objects. A single function can blow the stack with one declaration:
An 80 MB local array on a thread with an 8 MB stack overflows the moment the function is entered. The fix is to put large arrays on the heap, usually via std::vector:
Stack overflow tends to look like a sudden crash with no useful error message, especially on Linux. If a program crashes immediately on entering a function or in the middle of recursion, a stack overflow is a strong suspect.
Quick Check: Which of these is most likely to cause a stack overflow?
for (int i = 0; i < 1'000'000; i++) { new int(i); } (without deletes)std::vector of one million doubles<details> <summary>Answer</summary>
B. Unbounded recursion fills the stack frame by frame. Option A is a memory leak, not a stack issue. Option C puts the doubles on the heap, so the stack is unaffected.
</details>
The stack and heap behave very differently under load:
| Metric | Stack | Heap |
|---|---|---|
| Allocation cost | 1-2 instructions (adjust SP) | Often hundreds of instructions |
| Deallocation cost | 1-2 instructions (adjust SP) | Often hundreds of instructions |
| Cache behavior | Excellent (contiguous, hot) | Variable (scattered allocations) |
The stack pointer is a register, so allocating a stack frame is as fast as adding to a register. The heap allocator has more work: walk a free-list, possibly split a block, update metadata, possibly acquire a lock. Modern allocators (tcmalloc, jemalloc, the default glibc malloc) keep the overhead small for typical programs, but it remains measurable.
A microbenchmark showing the difference:
Sample Output (g++ -O0 on a modern laptop):
The exact numbers vary by compiler, optimization level, and allocator, but the stack version is consistently an order of magnitude faster. With -O2 the stack version may be optimized away entirely (the loop has no observable effect), while the heap version still pays the allocator cost on every iteration.
The cache behavior is similar. Stack frames are contiguous, used in a tight pattern, and almost always sit in L1 cache. Heap objects can be anywhere in memory, and following a chain of heap pointers can mean a cache miss on every step. A linked list of heap-allocated nodes is much slower than a std::vector of the same data, partly because the vector's elements are contiguous and prefetcher-friendly.
Prefer the stack when you have a choice. Use the heap when lifetime, size, or polymorphism requires it.
Q1: What is the main difference between stack and heap memory in C++?
Stack memory is managed automatically by the compiler. Local variables and function parameters live on the stack and are destroyed when their enclosing function returns. Heap memory is requested explicitly with new (or malloc) and stays alive until delete is called, regardless of which function is running. The stack is fast and limited in size (typically 1 to 8 MB per thread), while the heap is slower to allocate but can grow to many gigabytes.
Q2: Why does a function call push a new stack frame?
A stack frame holds everything the function needs that's specific to this call: its parameters, its local variables, and bookkeeping like the return address so the CPU knows where to resume in the caller. Each call needs its own copy, especially in recursion, where the same function is active multiple times simultaneously. The LIFO ordering of the stack matches the nested structure of function calls: the most recently called function is the first to return, so its frame is always on top.
Q3: What causes a stack overflow, and how do you avoid one?
Stack overflow happens when a thread uses more stack memory than the OS allotted to it. The two common causes are unbounded recursion (each call pushes a frame, eventually exceeding the limit) and very large local variables, like an array of millions of elements declared inside a function. To avoid recursion overflow, add a proper base case or convert the recursion to iteration. To avoid large-local overflow, put big arrays or buffers on the heap, typically through std::vector or smart pointers.
Q4: Why is heap allocation slower than stack allocation?
Stack allocation is a single CPU instruction: adjust the stack pointer register by the frame size. There's no metadata, no search, no lock. Heap allocation calls into the runtime's allocator, which has to find a free block of the right size, possibly split a larger block, update internal metadata, and (in multithreaded programs) acquire a lock. The cost is typically 10 to 100 times higher than a stack allocation. Heap memory also tends to scatter across the address space, which hurts CPU cache behavior compared to the tightly packed stack.
Q5: When should you choose heap over stack for an object?
Three situations push you to the heap. First, when the object needs to outlive the function that created it, since a stack object dies at function return. Second, when the size is only known at runtime and would not fit in a fixed local array. Third, when you need polymorphism through a base class pointer, since the derived object needs a stable heap address that a Base* can point at. Even then, modern C++ rarely uses raw new. A std::vector handles dynamic-size storage, and std::unique_ptr or std::shared_ptr handle the polymorphic case with automatic cleanup.
Exercise 1: Write a program that declares an int on the stack and an int on the heap, then prints both their values and their addresses.
Expected Output:
(Addresses will differ on each run.)
<details> <summary>Solution</summary>
</details>
Exercise 2: What does this program print? Explain why.
Expected Output:
<details> <summary>Solution</summary>
stock is a local variable inside makeStock. It lives in that function's stack frame. When makeStock returns, the frame is popped and the memory is no longer reserved for stock. The pointer p still holds the old address, but reading through it is undefined behavior. To fix it, return by value (return stock;) or allocate on the heap with new int(100) and remember to delete later.
</details>
Exercise 3: Fix the bug in this code so the function safely returns a Product.
<details> <summary>Solution</summary>
Return by value (simplest, modern style):
Or allocate on the heap and return a pointer the caller must delete:
The return-by-value form is preferred because modern compilers elide the copy through return value optimization.
</details>
Exercise 4: Write a recursive function countDown(int n) that prints n, n-1, ..., 1, then prints done. Run it with n = 5. Then try n = 1'000'000 and describe what happens.
Expected Output (n = 5):
<details> <summary>Solution</summary>
With n = 1'000'000, the program crashes with a stack overflow (segmentation fault on Linux). Each call pushes a frame, and a million frames easily exceeds the default 8 MB stack. The fix is to convert the recursion to a loop:
</details>
Exercise 5: Predict the output of this program. Then run it to check.
<details> <summary>Solution</summary>
globalCount is initialized to 0 and lives in the data segment. globalQuantity is uninitialized at file scope, so it lives in the BSS segment, which the OS zeroes at startup. staticDiscount has static storage duration; uninitialized statics are also zero-initialized. Only localPrice is uninitialized stack memory, so reading it is undefined behavior. The compiler may warn about reading an uninitialized local; the actual value printed is whatever was in that stack location.
</details>
Exercise 6: Write a function buildCart(int itemCount) that creates a std::vector<double> of prices with the given size, fills it with the values 1.99, 2.99, 3.99, ..., and returns it. Print the resulting prices in main.
Expected Output (itemCount = 4):
<details> <summary>Solution</summary>
The vector's backing storage is on the heap, but the vector handle itself lives on the stack of buildCart and is moved (or copied with NRVO eliding the copy) back to main. No raw new or delete is needed.
</details>
Exercise 7: What is wrong with this code? Fix it.
<details> <summary>Solution</summary>
new int[5] allocates an array, so the matching deallocation is delete[] prices;, not delete prices;. Using delete on a new[] allocation is undefined behavior. The fix:
Better still, use a std::vector<int> prices(5); and skip the raw allocation entirely.
</details>
Exercise 8: Write a program that triggers a stack overflow on purpose, and explain in a comment what made it overflow.
<details> <summary>Solution</summary>
Running this prints depths in steps of 1000 until the stack runs out, then the program crashes with a segmentation fault (Linux/macOS) or stack overflow exception (Windows). The exact depth where it crashes depends on the stack size limit and the per-frame size, which is influenced by the compiler and optimization level.
</details>
10 quizzes