Updated Jul 14, 2026

Constructors, Destructors & RAII

In Phase 6 you gave objects data and behavior. This phase gives them a birth and a death - code that runs automatically the moment an object comes into being, and code that runs automatically the moment it's gone. That second half, the automatic death, turns out to be the single most important idea in C++. It has a name - RAII - and once it clicks, it explains half of why C++ code looks the way it does.

The mental model: objects have a lifecycle

Every object in C++ goes through the same three moments, whether it's a local variable, a class member, or something on the heap:

  1. Construction - memory for the object exists, and now its constructor runs to set it up.
  2. Life - you use it.
  3. Destruction - the object's destructor runs, then the memory is reclaimed.

You've already seen construction and destruction happen for built-in things without thinking about it: a std::vector allocates its buffer when you create it and frees that buffer the instant it goes out of scope. That's not magic the language special-cased for vector. It's a constructor and a destructor, and you can write the exact same behavior into your own types.

What makes this different from C: in C, struct Buffer { char *data; } is just a data layout. Nothing runs when it's created, and nothing runs when it goes away - if data was malloc'd, you remember to free it, on every path out of the function, including the early return and the branch you added six months later and forgot about. C++ lets a type own that responsibility itself.

Constructors: setting up

A constructor is a special member function with the same name as the class and no return type. It runs automatically whenever an object is created.

class Point {
public:
    Point(double x, double y) : x_(x), y_(y) {
        std::cout << "Point constructed at (" << x_ << ", " << y_ << ")\n";
    }

private:
    double x_;
    double y_;
};

int main() {
    Point p(3.0, 4.0);   // constructor runs here, automatically
}
Point constructed at (3, 4)

Notice : x_(x), y_(y) before the constructor's { } body. That's the member initializer list, and it's not just style - it's how members get their first value. Without it, x_ and y_ would be left with indeterminate values first and then you'd overwrite them by assignment inside the body. For a primitive double that's merely redundant, but for a member like std::string it's genuinely wasteful: the member gets fully default-constructed and then reassigned - two steps where the list does one. Prefer the initializer list; use the body only for logic that isn't "just set this member."

💡 Key point. A class can have several constructors that differ by parameters - this is just overloading applied to construction. A constructor with no parameters is the default constructor; it's what runs for Point p; with nothing in the parentheses, if you provide one (or if the compiler can generate one for you - more on that in Phase 8).

⚠️ The explicit trap. A constructor taking exactly one argument doubles as an implicit conversion unless you stop it:

class Meters {
public:
    Meters(double value) : value_(value) {}
private:
    double value_;
};

void print(Meters m);

print(5.0);   // compiles! 5.0 silently becomes a Meters

That silent conversion is rarely what you want - it lets a plain double sneak into an API expecting a Meters with no visible sign it happened. Mark single-argument constructors explicit unless you specifically want the implicit conversion:

explicit Meters(double value) : value_(value) {}

print(5.0);          // error now: no implicit conversion
print(Meters(5.0));  // fine: you said it on purpose

Destructors: tearing down

A destructor is ~ClassName() - no parameters, no return type, and a class has at most one. It runs automatically the instant the object's lifetime ends: when a local variable's scope closes, when a member's owning object is destroyed, or when you delete a heap object.

class Point {
public:
    Point(double x, double y) : x_(x), y_(y) {}
    ~Point() {
        std::cout << "Point destroyed\n";
    }
private:
    double x_, y_;
};

void demo() {
    Point p(1.0, 2.0);
    std::cout << "using p\n";
}   // p goes out of scope right here

int main() {
    demo();
}
using p
Point destroyed

No call to destroy p anywhere in the code. The compiler inserted it at the closing } of demo, because that's where p's scope ends. This is deterministic destruction: you can point at the exact line where cleanup happens, before the program even runs. Compare that to a garbage-collected language, where an object becomes eligible for collection at that point but the actual cleanup happens whenever the collector next runs, on its own schedule, possibly much later. C++ gives you the same "when it's dead, it's cleaned up" guarantee, minus the "eventually."

📝 Terminology. People often say an object's destructor runs "when it goes out of scope." More precisely: it runs when the object's lifetime ends, which for a local variable is scope exit, but also happens for heap objects on delete, for members when their owning object is destroyed, and (critically) during stack unwinding - when an exception is thrown, every local object between the throw and the matching catch gets its destructor run, in order, on the way out. You'll see this again in Phase 15.

RAII: the idea destructors make possible

Here's the leap. If a destructor is guaranteed to run when an object dies, and it runs no matter which path the code takes out of scope (normal return, early return, break, or an exception unwinding through), then a destructor is the perfect place to release anything the object is holding onto: heap memory, a file handle, a mutex lock, a network socket. This pattern has a name:

RAII - Resource Acquisition Is Initialization. Acquire a resource in the constructor. Release it in the destructor. Tie the resource's lifetime to an object's lifetime, and let the compiler's automatic destruction do the releasing for you.

Watch the difference on a file handle. First, the C way:

void process_c(const char *path) {
    FILE *f = fopen(path, "r");
    if (!f) return;

    if (something_goes_wrong()) {
        return;              // leak: f is never closed
    }

    // ... use f ...
    fclose(f);
}

Every early exit is a place a fix could be forgotten - and in a function with five exit paths, someone eventually will. Now the RAII way:

class FileHandle {
public:
    explicit FileHandle(const char *path) : f_(std::fopen(path, "r")) {}
    ~FileHandle() {
        if (f_) std::fclose(f_);
    }
    FILE *get() const { return f_; }

private:
    FILE *f_;
};

void process_cpp(const char *path) {
    FileHandle f(path);
    if (!f.get()) return;

    if (something_goes_wrong()) {
        return;               // f_'s destructor still runs. No leak.
    }

    // ... use f.get() ...
}   // and here, on the normal path, same guarantee.

FileHandle's destructor runs on every exit from process_cpp - the early return, an exception, or falling off the end - because the compiler put the cleanup at the language level, not at each call site. You didn't write three copies of "remember to close the file." You wrote one destructor, once, and it covers every path forever, including paths someone adds next year.

This is why std::vector, std::string, and std::unique_ptr (coming in Phase 13) never need a manual free or delete: they're all RAII wrappers around a resource, exactly like FileHandle above, just written once by the standard library so nobody has to write it twice.

Construction and destruction order

When a class has several members, they're constructed in the order they're declared in the class - not the order they appear in the initializer list - and destroyed in the exact reverse order:

class Widget {
public:
    Widget() : a_(1), b_(2) {}   // a_ built first, then b_ (declaration order)
private:
    A a_;
    B b_;
};   // ~Widget() destroys b_ first, then a_

⚠️ Gotcha. If your initializer list writes b_(2), a_(1) - out of declaration order - most compilers will warn you (-Wreorder). The list's order in the source doesn't control construction order; the class's member declaration order does. Always write the initializer list in declaration order so the code reads the way it runs.

🪖 War story. A common bug: a member holds a raw pointer into another member declared later in the class, and the constructor tries to use it. Because members build top-to-bottom, that later member doesn't exist yet when the earlier one's constructor runs - the pointer is dangling from the start. The fix is almost always to reorder the member declarations, not to fight the initializer list.

Recap

  1. Every object has a lifecycle: construction (constructor runs), life, destruction (destructor runs) - and in C++ both are automatic and deterministic.
  2. Constructors set up an object; prefer the member initializer list over assignment in the body, and mark single-argument constructors explicit to block silent conversions.
  3. Destructors (~ClassName()) run the instant an object's lifetime ends - scope exit, delete, or exception unwinding - on every path, guaranteed.
  4. RAII ties a resource's lifetime to an object's lifetime: acquire in the constructor, release in the destructor. That single pattern is why C++ can manage memory, files, and locks without a garbage collector and without manual cleanup calls scattered through the code.
  5. Members are constructed in declaration order and destroyed in the reverse of that order, regardless of the initializer list's written order.

RAII answers "how does cleanup happen automatically." It doesn't yet answer what happens when you copy an RAII object - two owners now think they're responsible for freeing the same resource. That collision, and the five special functions that resolve it, is the crux of the whole language.

Quick check

Test yourself on the idea that makes this phase matter - that RAII ties cleanup to an object's lifetime instead of leaving it to a programmer's memory:

[
  {
    "q": "What does RAII actually guarantee?",
    "choices": [
      "A resource acquired in a constructor gets released in the destructor, automatically, on every exit path",
      "The compiler runs a garbage collector periodically to free unused objects",
      "Memory is freed the moment the last variable pointing to it is reassigned",
      "Resources are released only if the program exits normally, without an exception"
    ],
    "answer": 0,
    "explain": "RAII ties a resource's lifetime to an object's lifetime: the constructor acquires it, the destructor releases it, and the destructor is guaranteed to run on every path out of scope - normal return, early return, or exception unwinding."
  },
  {
    "q": "In `FileHandle f(path); if (!f.get()) return;`, why doesn't the early `return` leak the file handle the way the C version does?",
    "choices": [
      "The compiler runs `~FileHandle()` at that `return`, because that's where `f`'s lifetime ends, regardless of which exit path was taken",
      "The `return` statement automatically calls `fclose` on any open file handles it detects",
      "`FileHandle` doesn't actually leak in C either, the C example was just missing an `if`",
      "It only avoids the leak because `f.get()` happened to return null"
    ],
    "answer": 0,
    "explain": "Destruction is tied to lifetime, not to how a function exits. Every path out of `process_cpp` - including the early `return` - ends `f`'s lifetime at that point, so the compiler inserts the destructor call there too."
  },
  {
    "q": "A class has members `A a_;` then `B b_;`, and the constructor's initializer list writes `b_(2), a_(1)`. What order do construction and destruction actually happen in?",
    "choices": [
      "`a_` constructed first, then `b_`; destroyed in reverse - `b_` then `a_` - regardless of the initializer list's order",
      "`b_` constructed first because it's listed first in the initializer list, then `a_`",
      "Both are constructed in whatever order the compiler finds fastest",
      "Construction order follows the initializer list, but destruction order is unrelated to either"
    ],
    "answer": 0,
    "explain": "Declaration order controls construction order, not the order written in the initializer list - `a_` is declared first so it's built first, and destruction always reverses that: `b_` then `a_`."
  }
]

← Phase 6: Classes & Objects · Phase 8: Copy, Move & the Rule of Five →

Check your understanding 3 questions

1. What does RAII actually guarantee?

2. In `FileHandle f(path); if (!f.get()) return;`, why doesn't the early `return` leak the file handle the way the C version does?

3. A class has members `A a_;` then `B b_;`, and the constructor's initializer list writes `b_(2), a_(1)`. What order do construction and destruction actually happen in?