What you'll learn
Quick Answer
The stack holds function frames: parameters, local variables and return addresses. It is fast because allocating is just moving a register, and limited because each thread gets one fixed block, commonly a few megabytes. The heap is a large pool you allocate from explicitly and must release, which is where leaks come from. Deep recursion or one huge local array pushes past the stack limit and the program dies, usually reported as a segmentation fault.
Two regions with completely different rules
When your program runs, the operating system hands it a block of address space that gets divided into regions. Two of them matter for day-to-day programming.
The stack is a last-in-first-out region managed automatically. Every time a function is called, a frame is pushed onto it holding that call's parameters, its local variables, and the return address telling the CPU where to resume. When the function returns, the whole frame is discarded in one step. You never write code to manage this; the compiler emits it.
The heap is a large pool you allocate from on request, with new in C++ or malloc in C, and which stays yours until you release it. It has no automatic lifetime, which is both the point and the problem: an object on the heap can outlive the function that created it, and it will keep existing after you stop caring about it unless something frees it.
Two other regions are worth naming because variables live there too. Globals and static variables sit in a data segment that exists for the entire run of the program, neither stack nor heap. Your compiled instructions live in a text segment, and string literals usually sit in read-only memory near it, which is why writing through a char* that points at a literal crashes rather than silently editing it.
The distinction to hold onto is lifetime, not location. Stack means the compiler knows exactly when this dies. Heap means you decided when it dies. Everything else follows from that.
What actually lives where
Here is the layout for one small function, annotated:
#include <vector>
int total = 0; // data segment, lives for the whole program
static int callCount = 0; // data segment
void f(int n) { // n: stack (parameter)
int x = 5; // stack, 4 bytes
int arr[100]; // stack, 400 bytes
int* p = new int[100]; // p on the stack, the 400 bytes on the heap
std::vector<int> v(100); // v on the stack, its buffer on the heap
delete[] p; // v needs no such line
}The line worth pausing on is the last pair. A std::vector object is small, typically three pointers, and it sits on the stack like any other local. The elements it holds are on the heap. Same for std::string once the text is longer than the small buffer stored inside the string object itself.
That split explains something that otherwise looks arbitrary. This crashes:
void f() { int arr[1000000]; arr[0] = 1; } // ~4 MB on the stackand this is completely routine:
void g() { std::vector<int> arr(1000000); arr[0] = 1; } // ~4 MB on the heapThe stray write in each is only there so that the optimiser cannot delete an array nobody reads. Both hold a million integers. The first tries to reserve four megabytes inside a stack frame and blows past the limit. The second puts a small object on the stack whose buffer comes from the heap, where four megabytes is nothing.
This is also why competitive programmers declare large arrays at global scope. A global int arr[10000000] lives in the data segment, not the stack, so the same declaration that kills a local works fine outside any function, and it is zero-initialised for free.
Why the stack is fast and why it is small
Stack allocation is close to free. The CPU keeps a stack pointer in a register, and making room for a frame means subtracting a constant from it, usually one instruction. Releasing the frame is adding it back. There is no search, no bookkeeping, no metadata, and no possibility of fragmentation, because memory is always taken and returned at one end.
It is also fast for a second reason that matters more than the instruction count: locality. Your program keeps reusing the same few kilobytes at the top of the stack, so those lines are almost always sitting in the CPU's L1 cache. A heap allocation, by contrast, may land anywhere, and a cache miss costs far more than the allocation itself.
Heap allocation has to do real work. The allocator maintains a structure of free blocks, searches it for one large enough, splits it, records the size so free knows what to release, and in a multithreaded program may take a lock. If it has no suitable block it asks the operating system for more memory. Modern allocators are good, with per-thread caches for small sizes, but even their fast path is far more work than adjusting a register, and the slow path that goes back to the kernel is worse again.
The price of that speed is a hard ceiling. Each thread is given one stack when it is created and it cannot grow on demand. On many Linux systems the main thread gets 8 MB; you can see yours with ulimit -s, which prints kilobytes. On Windows the default is typically 1 MB, fixed in the executable header at link time. Threads you create yourself usually get less, and the size is a parameter of thread creation. The heap, by contrast, is bounded by the address space and available RAM.
What a stack overflow actually is
A stack overflow is not a mysterious error. It is the stack pointer walking past the end of the block your thread was given. Two things cause it.
The first is recursion without a working base case, or with a base case that some input never reaches:
int depth(int n) {
return depth(n + 1); // no base case: a new frame every call
}Build that with optimisation off if you want to watch it fail. This particular call sits in tail position, so at -O2 the compiler is free to turn it into a plain infinite loop that never grows the stack at all.
Do the arithmetic and the scale becomes concrete. If a frame takes 64 bytes and the stack is 8 MB, you get somewhere in the region of a hundred thousand nested calls before it ends. A frame holding a few local arrays is far larger, so the real number can be much smaller. Either way it is a fixed budget, not a slope you gradually slide down.
The second cause is a single frame that is too big on its own, which is the int arr[1000000] case from earlier. One call is enough.
What you see depends on the platform, and this trips people up. On Linux the guard page below the stack is unmapped, so the process receives SIGSEGV and the terminal prints Segmentation fault. There is no message saying stack overflow. On Windows you get a stack overflow exception, and a debugger will name it. If a program with recursion in it segfaults immediately on a large input, unbounded recursion is the first thing to check.
Managed languages behave differently because their runtime counts depth for you. Java raises StackOverflowError, and CPython raises RecursionError at a default limit of 1000 frames, adjustable with sys.setrecursionlimit. That limit is a safety net, not the real stack, so raising it far enough can crash the interpreter outright for the same underlying reason.
The fixes are the usual three: add or correct the base case, move large buffers off the stack into a std::vector or a global array, or rewrite the recursion iteratively with an explicit stack. A depth-first search over a graph of a hundred thousand nodes that happens to form a long chain is the classic case where the iterative version is not optional.
Leaks, fragmentation and who cleans up
Heap memory has the opposite failure mode. Nothing reclaims it automatically, so a leak is simply memory you allocated and then lost the last pointer to. Neither you nor the allocator can ever free it again.
void loadStudents() {
int* marks = new int[1000];
// ... work ...
} // marks goes out of scope; the 4000 bytes stayThe pointer was on the stack and died with the frame. The allocation it referred to did not. How much this matters depends entirely on how long the process lives. A competitive programming solution that runs for two seconds and exits can leak everything it likes, because the operating system reclaims the whole address space on exit. A web server or a game loop that leaks a little per request climbs until the machine starts swapping.
Fragmentation is the subtler cousin. After a long run of mixed allocations and frees, the free space is real but chopped into pieces, so a large request can fail even though the total free memory is ample. Long-running processes are the ones that feel this, which is why pooling and reusing buffers is common in servers and games.
Do not hunt leaks by eye. On Linux, valgrind --leak-check=full ./a.out reports every unfreed block with the stack trace that allocated it. GCC and Clang also support -fsanitize=address, which catches leaks along with out-of-bounds writes and use-after-free, and it is worth turning on for every debug build. MSVC has its own debug heap facilities.
The better answer is to not manage it manually. Use std::vector and std::string instead of raw arrays, and std::unique_ptr when you really need heap objects. Their destructors run when the owning stack frame unwinds, including during an exception, so the stack ends up managing the heap for you. And note that garbage-collected languages are not immune: a Java HashMap or a JavaScript array that only ever grows is unreachable memory in every practical sense, and it will exhaust the heap just as effectively.
