2. Memory: the Stack, the Heap & RAII¶
What & why¶
C++ has no garbage collector. Instead, every object has a precisely defined lifetime, and code runs automatically when that lifetime ends. Mastering this — and the idiom built on it, RAII — is the single most important step from "writes C++ syntax" to "writes C++".
Where objects live¶
| Storage | Created | Destroyed | Example |
|---|---|---|---|
| Automatic ("the stack") | at its declaration | at the end of its scope } |
int x = 5; inside a function |
| Dynamic ("the heap") | by new (or a container/smart pointer) |
by delete (or its owner) |
new Widget{} |
| Static | before main (or first use) |
after main returns |
globals, static locals |
The stack¶
Fast, automatic, limited in size (typically 1–8 MB):
void f() {
int a = 1; // created
std::string s = "hi"; // created
{
double d = 2.0; // created
} // d destroyed
} // s destroyed, then a (reverse order)
The heap¶
Large, flexible lifetime, and you decide when it ends:
int* p = new int{42}; // allocate + construct
std::cout << *p;
delete p; // destroy + free
int* arr = new int[100]; // array form
delete[] arr; // must match with delete[]
A std::vector<int> object itself lives on the stack, but its elements live on the heap. The vector manages that heap memory for you.
Why raw new/delete is dangerous¶
void process() {
Widget* w = new Widget{};
if (!w->load("config.txt")) {
return; // LEAK: forgot delete on this path
}
w->run(); // if this throws → LEAK
delete w;
}
Manual memory management fails in predictable ways:
- Leak — never
deleted. - Double free —
deleted twice. - Use after free — used after
delete. - Mismatch —
new[]withdelete, or vice versa.
The fix isn't "be more careful". It's to make cleanup automatic.
Destructors¶
A class can define a destructor, ~ClassName(), which runs automatically when an object's lifetime ends — at scope exit, during exception unwinding, or when its owner is destroyed:
#include <iostream>
struct Noisy {
std::string name;
explicit Noisy(std::string n) : name{std::move(n)} {
std::cout << "construct " << name << '\n';
}
~Noisy() {
std::cout << "destroy " << name << '\n';
}
};
int main() {
Noisy a{"a"};
{
Noisy b{"b"};
}
Noisy c{"c"};
}
Destruction happens in reverse order of construction, deterministically. That's the guarantee RAII is built on.
RAII: Resource Acquisition Is Initialization¶
Tie every resource to the lifetime of an object. Acquire it in the constructor, release it in the destructor. Then the resource can't leak, even on early returns or exceptions.
"Resource" means anything that must be released: heap memory, file handles, sockets, locks, database transactions, GPU buffers.
A small RAII wrapper around a C file handle:
#include <cstdio>
#include <stdexcept>
#include <string>
class File {
public:
File(const std::string& path, const char* mode)
: handle_{std::fopen(path.c_str(), mode)} {
if (handle_ == nullptr) {
throw std::runtime_error("cannot open " + path);
}
}
~File() {
std::fclose(handle_);
}
// Copying would lead to a double fclose — forbid it (lesson 5 explains).
File(const File&) = delete;
File& operator=(const File&) = delete;
void write(const std::string& text) {
std::fputs(text.c_str(), handle_);
}
private:
std::FILE* handle_;
};
void log_twice() {
File f{"log.txt", "a"};
f.write("one\n");
f.write("two\n");
} // fclose happens here — always, even if write() threw
You already use RAII types all the time:
| Resource | RAII type |
|---|---|
| heap memory (many objects) | std::vector, std::string, std::map, ... |
| heap memory (one object) | std::unique_ptr, std::shared_ptr (next lesson) |
| files | std::ifstream, std::ofstream |
| mutex locks | std::lock_guard, std::scoped_lock |
| threads | std::jthread |
Scope guards for one-off cleanup¶
When there's no ready-made type, a tiny generic guard does the job:
#include <utility>
template <typename F>
class ScopeExit {
public:
explicit ScopeExit(F f) : f_{std::move(f)} {}
~ScopeExit() { f_(); }
ScopeExit(const ScopeExit&) = delete;
ScopeExit& operator=(const ScopeExit&) = delete;
private:
F f_;
};
void work() {
start_timer();
ScopeExit stop{[] { stop_timer(); }};
// ... any return or throw here still stops the timer
}
Stack vs. heap: which to use?¶
Default to automatic (stack) objects and standard containers. Reach for dynamic allocation only when:
- the object must outlive the current scope and can't simply be returned by value,
- the object is very large (big arrays →
std::vector), - you need runtime polymorphism through a base-class pointer (lesson 7),
- the size or number of objects is only known at runtime (containers handle this).
And when you do allocate, let an owner (container or smart pointer) do it — never a naked new.
Object lifetime rules to remember¶
- Temporaries die at the end of the full expression (the
;), unless bound to aconst&or&&local, which extends their life. - Members are constructed in declaration order and destroyed in reverse.
staticlocal variables are constructed the first time control passes through them (thread-safely) and destroyed aftermain.
Common mistakes¶
- Writing
newin application code. Usestd::make_unique/ containers. - Mismatched
new[]/delete. - Big arrays on the stack —
int big[10'000'000];overflows the stack. Usestd::vector. - Destructors that throw — if one throws during exception unwinding, the program terminates. Destructors are
noexceptby default; keep them that way. - Copying an RAII type that owns a raw handle — two destructors free it twice. Delete or define the copy operations (lesson 5).
- Returning a pointer to a stack object.
Exercises¶
- Add
Noisyobjects as members of another struct and as elements of astd::vector<Noisy>. Predict the construct/destroy order, then run it. What happens when the vector grows? - Write a function that uses
newand has two early returns and one possible throw. Run it under-fsanitize=addressand see the leak report. Rewrite withstd::vectororstd::unique_ptrand confirm the leak disappears. - Implement a
TimerRAII class that prints how long its scope took when destroyed. Use it to time a sort. - Write an RAII
DirectoryChangerthat changes the current directory (std::filesystem::current_path(p)) in its constructor and restores the old one in its destructor. - Show that a destructor runs during exception unwinding: construct a
Noisy, then throw, and catch inmain.
Recap¶
- Objects have automatic (scope), dynamic (
new/delete), or static lifetime. - Destructors run deterministically, in reverse construction order — including during exception unwinding.
- RAII: acquire in the constructor, release in the destructor. Every resource gets an owning object.
- Standard containers, streams, smart pointers, and lock guards are all RAII.
- Never write naked
new/deletein everyday code.
Next: Smart pointers →