C++ Constructors and Destructors Explained — With Real-World Patterns
Every program manages resources — memory, file handles, network connections, database locks. In C, you had to manually open every resource and manually close it, and if anything went wrong in between, you were left with leaks that could crash servers and corrupt data. C++ was designed from the ground up to fix this. Constructors and destructors are the foundation of that fix. They're not just syntax sugar — they're the mechanism that makes C++ the language of choice for systems where resource safety is non-negotiable, from game engines to operating systems to financial trading platforms.
The core problem they solve is predictability. Without a guaranteed setup and teardown mechanism, object state is fragile — you rely on the programmer to remember to call init() before using an object and cleanup() afterward. That's two extra opportunities for bugs per object, per use. Constructors guarantee that an object is fully initialised the instant it exists. Destructors guarantee that cleanup happens the instant an object goes out of scope, no matter how the scope exits — whether normally, or via an exception.
By the end of this article you'll understand not just how to write constructors and destructors, but why each type exists, when to reach for each one, how the RAII pattern uses them to make resource management automatic, and the exact mistakes that cause memory leaks and double-free crashes in real codebases. You'll be able to reason about object lifetime confidently — and that's what separates intermediate C++ developers from beginners.
What Constructors Actually Do (and Why You Can't Skip Them)
A constructor is a special member function that runs automatically the moment an object is created. It has the same name as the class and no return type — not even void. That 'no return type' rule isn't arbitrary: the language designers wanted it to be unmistakably distinct from regular functions, because it behaves differently. It doesn't return a value to a caller; it initialises the object in place.
The most important thing to understand is that C++ guarantees the constructor runs before any code can use the object. This makes it impossible to accidentally use an uninitialised object — if construction succeeds, the object is ready. If it fails (for example, memory allocation inside the constructor throws), the object never exists in the first place. That's a powerful guarantee.
C++ gives you several constructor types for different situations: the default constructor (no arguments), parameterised constructors (take arguments to set initial state), the copy constructor (creates a new object from an existing one), and the move constructor (transfers ownership from a temporary). Each exists because a different creation scenario has different performance and semantic needs. Reaching for the right one matters — especially when objects hold expensive resources like heap memory or file handles.
#include <iostream> #include <string> class BankAccount { private: std::string ownerName; double balance; int accountNumber; public: // DEFAULT CONSTRUCTOR — called when no arguments are provided // Use this when a 'blank' account makes sense in your domain BankAccount() : ownerName("Unknown"), balance(0.0), accountNumber(0) { std::cout << "[Default Constructor] Empty account created.\n"; } // PARAMETERISED CONSTRUCTOR — called with specific initial values // This is the most common type you'll write in real code BankAccount(const std::string& name, double initialDeposit, int accNum) : ownerName(name), balance(initialDeposit), accountNumber(accNum) { // The initialiser list above sets members BEFORE the constructor body runs // This is more efficient than assigning inside the body std::cout << "[Parameterised Constructor] Account created for " << ownerName << " with balance $" << balance << "\n"; } void printDetails() const { std::cout << "Account #" << accountNumber << " | Owner: " << ownerName << " | Balance: $" << balance << "\n"; } }; int main() { // Calls the DEFAULT constructor BankAccount emptyAccount; // Calls the PARAMETERISED constructor BankAccount aliceAccount("Alice Johnson", 5000.00, 10042); emptyAccount.printDetails(); aliceAccount.printDetails(); return 0; }
[Parameterised Constructor] Account created for Alice Johnson with balance $5000
Account #0 | Owner: Unknown | Balance: $0
Account #10042 | Owner: Alice Johnson | Balance: $5000
Destructors and RAII — The Pattern That Makes C++ Resource-Safe
A destructor is the mirror of a constructor. It runs automatically when an object's lifetime ends — when it goes out of scope on the stack, or when delete is called on a heap-allocated object. It has the same name as the class, prefixed with a tilde (~), takes no parameters, and returns nothing. You can only have one per class.
The reason destructors matter so deeply is a design pattern called RAII — Resource Acquisition Is Initialisation. The idea: tie the lifetime of a resource directly to the lifetime of an object. Acquire the resource in the constructor, release it in the destructor. Because the destructor is guaranteed to run when the object goes out of scope — including when an exception unwinds the stack — you get automatic, exception-safe cleanup for free.
This is how std::fstream, std::unique_ptr, std::lock_guard, and most of the C++ standard library work. You don't call file.close() manually because the destructor handles it. This eliminates an entire class of bugs. If you ever find yourself writing a class that acquires a resource (heap memory, a file, a mutex, a socket), your first thought should be: 'I need a destructor.' That instinct is what separates memory-safe C++ from the kind that leaks.
#include <iostream> #include <fstream> #include <string> #include <stdexcept> // RAII in action: this class manages a log file. // The file opens in the constructor and closes in the destructor — // no matter how the function using it exits. class FileLogger { private: std::ofstream logFile; // the resource we're managing std::string filePath; public: // CONSTRUCTOR: acquire the resource explicit FileLogger(const std::string& path) : filePath(path) { logFile.open(filePath, std::ios::app); if (!logFile.is_open()) { // Throwing from a constructor is correct — the object won't exist // if construction fails, so the destructor won't run (no double-close) throw std::runtime_error("Could not open log file: " + filePath); } std::cout << "[Constructor] Log file opened: " << filePath << "\n"; } // DESTRUCTOR: release the resource automatically ~FileLogger() { if (logFile.is_open()) { logFile.close(); // This runs even if an exception was thrown elsewhere in the program std::cout << "[Destructor] Log file closed: " << filePath << "\n"; } } void log(const std::string& message) { logFile << message << "\n"; std::cout << "[Log] " << message << "\n"; } // Delete copy operations — a file handle shouldn't be shared FileLogger(const FileLogger&) = delete; FileLogger& operator=(const FileLogger&) = delete; }; void runAppSession() { // logger is a stack object — its destructor runs when this function returns FileLogger logger("app_session.log"); logger.log("Session started."); logger.log("User logged in: alice@example.com"); // Imagine an early return or exception here — the file STILL closes correctly } // <-- destructor fires here automatically int main() { std::cout << "--- Starting session ---\n"; runAppSession(); std::cout << "--- Session ended, file safely closed ---\n"; return 0; }
[Constructor] Log file opened: app_session.log
[Log] Session started.
[Log] User logged in: alice@example.com
[Destructor] Log file closed: app_session.log
--- Session ended, file safely closed ---
Copy, Move, and the Rule of Five — When the Compiler Gets It Wrong
If your class manages a raw resource (a raw pointer, a file descriptor, a socket), the compiler-generated copy constructor will do a shallow copy — it copies the pointer value, not the data it points to. Now two objects think they own the same memory. When both destructors run, you get a double-free crash. This is one of the most common sources of undefined behaviour in C++.
The Rule of Five says: if you define any one of — destructor, copy constructor, copy assignment operator, move constructor, move assignment operator — you almost certainly need to define all five. They're a package deal because they all relate to ownership semantics.
The move constructor and move assignment operator (added in C++11) are the performance piece. Instead of deep-copying an expensive resource from a temporary object that's about to be destroyed anyway, you transfer ownership. Think of it as handing over the keys rather than making a duplicate set. This is why inserting a std::vector of a million entries into another vector is fast — moves happen instead of copies wherever the compiler knows the source is a temporary.
#include <iostream> #include <cstring> // for memcpy #include <utility> // for std::move // A class that owns a heap-allocated buffer — classic Rule of Five scenario class ManagedBuffer { private: char* data; // raw resource we own size_t size; public: // --- CONSTRUCTOR --- explicit ManagedBuffer(size_t bufferSize) : size(bufferSize), data(new char[bufferSize]()) { std::cout << "[Constructor] Allocated " << size << " bytes at " << static_cast<void*>(data) << "\n"; } // --- DESTRUCTOR --- ~ManagedBuffer() { std::cout << "[Destructor] Freeing memory at " << static_cast<void*>(data) << "\n"; delete[] data; // always use delete[] for arrays data = nullptr; // defensive: prevents dangling pointer use-after-free } // --- COPY CONSTRUCTOR (deep copy) --- // Without this, both objects point to the same memory — double-free waiting to happen ManagedBuffer(const ManagedBuffer& other) : size(other.size), data(new char[other.size]) { std::memcpy(data, other.data, size); // copy the actual data, not just the pointer std::cout << "[Copy Constructor] Copied " << size << " bytes to " << static_cast<void*>(data) << "\n"; } // --- COPY ASSIGNMENT OPERATOR --- ManagedBuffer& operator=(const ManagedBuffer& other) { if (this == &other) return *this; // self-assignment guard — always include this delete[] data; // release old resource before taking new one size = other.size; data = new char[size]; std::memcpy(data, other.data, size); std::cout << "[Copy Assignment] Assigned " << size << " bytes\n"; return *this; } // --- MOVE CONSTRUCTOR (transfer ownership, no allocation needed) --- ManagedBuffer(ManagedBuffer&& other) noexcept : size(other.size), data(other.data) { other.data = nullptr; // leave the source in a valid but empty state other.size = 0; std::cout << "[Move Constructor] Ownership transferred\n"; } // --- MOVE ASSIGNMENT OPERATOR --- ManagedBuffer& operator=(ManagedBuffer&& other) noexcept { if (this == &other) return *this; delete[] data; // release what we currently own data = other.data; // steal the resource size = other.size; other.data = nullptr; // neutralise the source other.size = 0; std::cout << "[Move Assignment] Ownership transferred\n"; return *this; } size_t getSize() const { return size; } }; int main() { ManagedBuffer original(256); std::cout << "\n-- Deep Copy --\n"; ManagedBuffer copyOfOriginal(original); // calls Copy Constructor std::cout << "\n-- Move (no allocation) --\n"; ManagedBuffer movedBuffer(std::move(original)); // calls Move Constructor // 'original' is now empty (data == nullptr) — safe but don't use it std::cout << "\n-- Cleanup --\n"; // All three destructors run here — each frees its own memory safely return 0; }
-- Deep Copy --
[Copy Constructor] Copied 256 bytes to 0x55a3c1f2e3d0
-- Move (no allocation) --
[Move Constructor] Ownership transferred
-- Cleanup --
[Destructor] Freeing memory at 0x55a3c1f2e3d0
[Destructor] Freeing memory at 0x55a3c1f2e2c0
[Destructor] Freeing memory at (nil)
| Aspect | Constructor | Destructor |
|---|---|---|
| Purpose | Initialise object state and acquire resources | Release resources and clean up object state |
| When it runs | Automatically when the object is created | Automatically when the object goes out of scope or is deleted |
| Return type | None (not even void) | None (not even void) |
| Parameters allowed | Yes — can be overloaded with multiple versions | No — exactly one, takes no parameters |
| Can be overloaded | Yes — default, parameterised, copy, move | No — only one destructor per class |
| Can throw exceptions | Yes — throwing prevents the object from existing | Should never throw — wrap risky code in try-catch |
| Inheritance behaviour | Base class constructor runs FIRST | Base class destructor runs LAST (reverse order) |
| Virtual keyword | Never virtual — type isn't fully known yet | Should be virtual in base classes with virtual functions |
🎯 Key Takeaways
- Constructors guarantee an object is fully initialised before any code can use it — construction failure (via exception) means the object never exists, so there's no broken half-initialised state to clean up.
- Destructors run automatically when an object's lifetime ends, including during stack unwinding from exceptions — this is the mechanism that makes RAII work and eliminates entire classes of resource-leak bugs.
- The Rule of Five: if your class needs a custom destructor, it almost certainly needs a custom copy constructor, copy assignment, move constructor, and move assignment too — because all five relate to ownership of the same resource.
- Prefer the Rule of Zero: design classes around standard library types (
std::unique_ptr,std::string,std::vector) so the compiler-generated special members do the right thing, and you never write a destructor at all.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Missing
virtualon the base class destructor — If you delete a derived class object through a base class pointer and the destructor isn't virtual, only the base destructor runs. The derived class destructor is silently skipped, leaking every resource the derived class owns. Fix: always declarevirtual ~BaseClass() {}in any class you intend to be inherited from. - ✕Mistake 2: Relying on the compiler-generated copy constructor for classes with raw pointers — The default copy constructor copies the pointer address, not the pointed-to data. Both objects now share the same memory. When one is destroyed, the other holds a dangling pointer, and the second destructor causes a double-free crash or silent heap corruption. Fix: follow the Rule of Five — write a deep-copy copy constructor whenever your class owns a raw pointer.
- ✕Mistake 3: Throwing exceptions from destructors — If a destructor throws while the stack is already unwinding due to another exception, C++ immediately calls
std::terminate(). There's no catch, no recovery — your process dies. Fix: wrap any potentially-throwing cleanup logic in a try-catch inside the destructor, log the error, and proceed. Destructors must benoexceptin spirit even if not declared that way.
Interview Questions on This Topic
- QWhat is the Rule of Five in C++, and why does defining a custom destructor imply you probably need all five special member functions?
- QWhat happens if you delete a derived class object through a base class pointer when the base class destructor is not virtual? Walk me through exactly what goes wrong.
- QExplain RAII. How do constructors and destructors make it work, and why does this pattern make C++ code more exception-safe than manual resource management?
Frequently Asked Questions
What is the difference between a constructor and a destructor in C++?
A constructor runs automatically when an object is created and is responsible for initialising the object's state and acquiring any resources it needs. A destructor runs automatically when the object's lifetime ends and is responsible for releasing those resources. They're two halves of the same lifecycle contract — acquire in construction, release in destruction.
Can a C++ constructor call another constructor in the same class?
Yes — since C++11, this is called constructor delegation. You write BankAccount() : BankAccount("Unknown", 0.0, 0) {} to have the default constructor forward to a parameterised one. It avoids duplicating initialisation logic. Only the delegated-to constructor's initialiser list runs; you can't combine both lists.
Why should destructors be virtual in base classes?
When you delete a derived class object through a base class pointer, C++ uses the static type (the base) to decide which destructor to call — unless it's virtual. Without virtual, only the base destructor runs, silently skipping the derived destructor and leaking every resource the derived class manages. Any class with at least one virtual function should have a virtual destructor.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.