Home C / C++ C++ Constructors and Destructors Explained — With Real-World Patterns

C++ Constructors and Destructors Explained — With Real-World Patterns

In Plain English 🔥
Think of a constructor like a hotel check-in desk. The moment you arrive (an object is created), someone hands you a room key, sets up your Wi-Fi, and turns on the lights — everything is ready before you even walk to your room. A destructor is the check-out process: you hand back the key, the room gets cleaned, and resources are freed for the next guest. You never have to remember to 'clean up' yourself — the hotel handles it automatically when you leave.
⚡ Quick Answer
Think of a constructor like a hotel check-in desk. The moment you arrive (an object is created), someone hands you a room key, sets up your Wi-Fi, and turns on the lights — everything is ready before you even walk to your room. A destructor is the check-out process: you hand back the key, the room gets cleaned, and resources are freed for the next guest. You never have to remember to 'clean up' yourself — the hotel handles it automatically when you leave.

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.

BankAccount.cpp · CPP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
#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;
}
▶ Output
[Default Constructor] Empty account created.
[Parameterised Constructor] Account created for Alice Johnson with balance $5000
Account #0 | Owner: Unknown | Balance: $0
Account #10042 | Owner: Alice Johnson | Balance: $5000
⚠️
Pro Tip: Always Use the Member Initialiser ListWrite `BankAccount(int n) : accountNumber(n) {}` instead of `BankAccount(int n) { accountNumber = n; }`. The initialiser list directly constructs members, while the body version default-constructs them first and then assigns — that's a wasted construction for non-trivial types like `std::string`. For `const` members and reference members, the initialiser list isn't optional — it's the only way.

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.

FileLogger.cpp · CPP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
#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;
}
▶ Output
--- Starting session ---
[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 ---
⚠️
Watch Out: Never Let Destructors ThrowIf a destructor throws an exception while the stack is already unwinding from another exception, C++ calls `std::terminate()` and your program dies instantly. Always wrap anything that can fail inside a destructor in a try-catch block and swallow or log the error. The destructor's job is cleanup — not error reporting.

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.

ManagedBuffer.cpp · CPP
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
#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;
}
▶ Output
[Constructor] Allocated 256 bytes at 0x55a3c1f2e2c0

-- 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)
🔥
Interview Gold: The Rule of ZeroThe Rule of Five has a corollary called the Rule of Zero: if you design your class to use only standard library members (like `std::unique_ptr` and `std::string`) that already manage their own resources, you don't need to write any of the five special functions at all. The compiler-generated defaults will do the right thing. Prefer the Rule of Zero — only reach for the Rule of Five when you're genuinely managing a raw resource.
AspectConstructorDestructor
PurposeInitialise object state and acquire resourcesRelease resources and clean up object state
When it runsAutomatically when the object is createdAutomatically when the object goes out of scope or is deleted
Return typeNone (not even void)None (not even void)
Parameters allowedYes — can be overloaded with multiple versionsNo — exactly one, takes no parameters
Can be overloadedYes — default, parameterised, copy, moveNo — only one destructor per class
Can throw exceptionsYes — throwing prevents the object from existingShould never throw — wrap risky code in try-catch
Inheritance behaviourBase class constructor runs FIRSTBase class destructor runs LAST (reverse order)
Virtual keywordNever virtual — type isn't fully known yetShould 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 virtual on 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 declare virtual ~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 be noexcept in 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.

🔥
TheCodeForge Editorial Team Verified Author

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.

← PreviousClasses and Objects in C++Next →Inheritance in C++
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged