Skip to content

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[] with delete, 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"};
}
construct a
construct b
destroy b
construct c
destroy c
destroy a

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 a const& or && local, which extends their life.
  • Members are constructed in declaration order and destroyed in reverse.
  • static local variables are constructed the first time control passes through them (thread-safely) and destroyed after main.

Common mistakes

  • Writing new in application code. Use std::make_unique / containers.
  • Mismatched new[] / delete.
  • Big arrays on the stack — int big[10'000'000]; overflows the stack. Use std::vector.
  • Destructors that throw — if one throws during exception unwinding, the program terminates. Destructors are noexcept by 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

  1. Add Noisy objects as members of another struct and as elements of a std::vector<Noisy>. Predict the construct/destroy order, then run it. What happens when the vector grows?
  2. Write a function that uses new and has two early returns and one possible throw. Run it under -fsanitize=address and see the leak report. Rewrite with std::vector or std::unique_ptr and confirm the leak disappears.
  3. Implement a Timer RAII class that prints how long its scope took when destroyed. Use it to time a sort.
  4. Write an RAII DirectoryChanger that changes the current directory (std::filesystem::current_path(p)) in its constructor and restores the old one in its destructor.
  5. Show that a destructor runs during exception unwinding: construct a Noisy, then throw, and catch in main.

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/delete in everyday code.

Next: Smart pointers →