C++
Constructors, Destructors & RAII
Classes & Objects introduced the constructor briefly, as the method that runs automatically on creation. This chapter is the full picture: what runs at the other end of an object's life too, and the single idiom — RAII — that both enable, which quietly eliminates an entire category of bug that's constant, manual work in C.
- What it is
- A constructor runs automatically when an object is created; a destructor runs automatically when it's destroyed. RAII (Resource Acquisition Is Initialization) is the pattern of tying a resource's lifetime to an object's, using exactly these two.
- Mental model
- A constructor is move-in day — the object sets itself up. A destructor is move-out day — it happens automatically, even if you "forget," the moment the object's scope ends.
- Key numbers
- Every object gets exactly one destructor call, guaranteed, when it goes out of scope — no matter how many exit paths (early
return, exception) the function has. - Reach for it when
- An object owns a resource — memory, a file handle, a lock, a hardware peripheral — that must be released exactly once, no matter how the function exits.
Constructors, in more depth
class Buffer {
public:
Buffer(int size) { // constructor — takes arguments like any function
data = new int[size]; // allocate on construction
capacity = size;
std::cout << "Buffer created\n";
}
private:
int *data;
int capacity;
};
Buffer b(100); // constructor runs immediately — data is allocated before the next line runs
A constructor can take parameters exactly like any function, letting an object configure itself at creation time instead of needing a separate, easy-to-forget "init" call — a pattern C code has to write by hand every time.
Destructors
class Buffer {
public:
Buffer(int size) {
data = new int[size];
std::cout << "Buffer created\n";
}
~Buffer() { // destructor — same name as the class, prefixed with ~
delete[] data; // free exactly what the constructor allocated
std::cout << "Buffer destroyed\n";
}
private:
int *data;
};
void useBuffer() {
Buffer b(100);
// ... use b ...
} // b goes out of scope HERE — ~Buffer() runs automatically, data is freed
The destructor runs the instant an object's scope ends — no explicit call needed, and no way to
accidentally skip it on a normal exit path. This is the direct C++ answer to the exact
Dynamic Memory problem of a forgotten
free(): pair every resource's acquisition with a matching release, in one place, guaranteed
to run.
RAII — the idiom both enable
RAII (Resource Acquisition Is Initialization) is the practice of acquiring a resource in a constructor and releasing it in the matching destructor — tying the resource's lifetime directly to an object's lifetime, which C++ already guarantees is well-defined and automatic:
class FileHandle {
public:
FileHandle(const char *path) {
file = fopen(path, "r"); // ACQUIRE in the constructor
}
~FileHandle() {
if (file) fclose(file); // RELEASE in the destructor — always runs
}
FILE *get() { return file; }
private:
FILE *file;
};
void readConfig() {
FileHandle f("config.txt"); // file opened
// ... use f.get() ...
// even if an exception is thrown here, ~FileHandle() STILL runs
} // file closed automatically, guaranteed, right here
Compare this to the equivalent C file-handling code, where
every single exit path — including an early return on an error — has to remember its own
matching fclose. RAII makes "forgetting to clean up" a non-issue by construction, not a
discipline problem.
An object's full lifecycle
Real-world applications
- Smart pointers —
std::unique_ptrandstd::shared_ptr(covered fully in the upcoming Memory Management chapter) are RAII applied to raw memory itself. - Lock guards — a mutex lock acquired in a constructor and released in a destructor guarantees the lock is released even if an exception unwinds through the function — a real, common concurrency-safety pattern.
- Hardware peripheral wrappers — a class that enables a peripheral in its constructor and disables it in its destructor, ensuring a peripheral is never left powered on by a forgotten cleanup call.
Gotchas
- A class managing a resource (like
Bufferabove) that doesn't also define a proper copy behavior can lead to double-free bugs — copying the object copies the raw pointer, and both copies' destructors then try to free the same memory. This is exactly what the upcoming Operator Overloading & References chapter's coverage of copy semantics addresses. - A destructor should never throw an exception — if it does during the unwinding caused by another exception, the program terminates immediately, since C++ can't safely decide which exception should "win."
- Global objects' constructors run before
main()starts, and destructors after it returns — order between different global objects across files is not guaranteed, a genuine, sometimes-surprising source of bugs ("static initialization order fiasco"). - RAII only protects resources actually owned by an object with a real destructor — a raw pointer assigned with plain
newand never wrapped in a class gets none of these guarantees; see the upcoming Memory Management chapter.