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

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

Where developers are forged. · Structured learning · Free forever.
📍 Part of: C++ Basics → Topic 4 of 19
Master C++ object lifecycles: from RAII and the Rule of Five to constructor delegation and virtual destructors.
⚙️ Intermediate — basic C / C++ knowledge assumed
In this tutorial, you'll learn
Master C++ object lifecycles: from RAII and the Rule of Five to constructor delegation and virtual destructors.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
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; 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. 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, how the RAII pattern makes resource management automatic, and the exact mistakes that cause memory leaks and double-free crashes in production codebases.

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. This '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 fails (for example, a new allocation inside the constructor throws), the object never exists in the first place, ensuring you never have to deal with 'zombie' objects in half-broken states.

C++ provides several constructor types: the default constructor (no arguments), parameterised constructors (custom state), the copy constructor (cloning), and the move constructor (ownership transfer). Reaching for the right one is a matter of performance—especially when objects hold expensive resources like heap memory or socket handles.

BankAccount.cpp · CPP
1234567891011121314151617181920212223242526272829303132333435363738
/* 
 * Copyright (c) 2026 TheCodeForge.io
 * Package: io.thecodeforge.finance
 */
#include <iostream>
#include <string>

namespace io_thecodeforge {
    class BankAccount {
    private:
        std::string ownerName;
        double balance;
        int accountNumber;

    public:
        // DEFAULT CONSTRUCTOR - Uses Delegation to minimize code duplication
        BankAccount() : BankAccount("Unknown", 0.0, 0) {
            std::cout << "[Default] Delegated to parameterized constructor.\n";
        }

        // PARAMETERISED CONSTRUCTOR - Uses Member Initializer List (Efficient)
        BankAccount(const std::string& name, double initialDeposit, int accNum)
            : ownerName(name), balance(initialDeposit), accountNumber(accNum) {
            std::cout << "[Parameterized] Created account: " << accountNumber << "\n";
        }

        void printDetails() const {
            std::cout << "Owner: " << ownerName << " | Balance: $" << balance << "\n";
        }
    };
}

int main() {
    using namespace io_thecodeforge;
    BankAccount aliceAccount("Alice Johnson", 5000.00, 10042);
    aliceAccount.printDetails();
    return 0;
}
▶ Output
[Parameterized] Created account: 10042
Owner: Alice Johnson | Balance: $5000
💡Pro Tip: Always Use the Member Initialiser List
Write 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 cycle for non-trivial types like std::string. For const and reference members, the initialiser list is mandatory.

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, and std::lock_guard work. You don't call file.close() manually because the destructor handles it. This eliminates an entire class of bugs. If your class acquires a resource (heap memory, a file, a mutex, a socket), you need a destructor.

FileLogger.cpp · CPP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
/* 
 * Copyright (c) 2026 TheCodeForge.io
 * Package: io.thecodeforge.logging
 */
#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>

namespace io_thecodeforge {
    class FileLogger {
    private:
        std::ofstream logFile;
        std::string   filePath;

    public:
        explicit FileLogger(const std::string& path) : filePath(path) {
            logFile.open(filePath, std::ios::app);
            if (!logFile.is_open()) {
                throw std::runtime_error("Failed to open log: " + filePath);
            }
            std::cout << "[RAII] File resource acquired.\n";
        }

        // Destructor: The 'Teardown' phase
        ~FileLogger() {
            if (logFile.is_open()) {
                logFile.close();
                std::cout << "[RAII] File resource released automatically.\n";
            }
        }

        void log(const std::string& message) {
            logFile << message << "\n";
        }

        // Prevent accidental copying (would cause double-close issues)
        FileLogger(const FileLogger&) = delete;
        FileLogger& operator=(const FileLogger&) = delete;
    };
}

int main() {
    { 
        io_thecodeforge::FileLogger logger("session.log");
        logger.log("System check OK.");
    } // Destructor fires here as logger goes out of scope
    return 0;
}
▶ Output
[RAII] File resource acquired.
[RAII] File resource released automatically.
⚠ Watch Out: Never Let Destructors Throw
If 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 potentially failing cleanup inside 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), the compiler-generated copy constructor will do a shallow copy. 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 states: if you define any one of — destructor, copy constructor, copy assignment, move constructor, or move assignment — you almost certainly need to define all five. They are a package deal because they all relate to resource ownership semantics.

The move constructor and move assignment operator (C++11) are the performance piece. Instead of deep-copying an expensive resource from a temporary object, you 'steal' the resource and leave the temporary in a null state. It's the difference between duplicating a 1GB file and simply renaming it.

ManagedBuffer.cpp · CPP
123456789101112131415161718192021222324252627282930313233343536373839404142434445
/* 
 * Copyright (c) 2026 TheCodeForge.io
 * Package: io.thecodeforge.memory
 */
#include <iostream>
#include <cstring>
#include <utility>

namespace io_thecodeforge {
    class ManagedBuffer {
    private:
        char* data;
        size_t size;

    public:
        explicit ManagedBuffer(size_t bufferSize)
            : size(bufferSize), data(new char[bufferSize]()) {}

        ~ManagedBuffer() {
            delete[] data;
        }

        // Rule of 5: Deep Copy Constructor
        ManagedBuffer(const ManagedBuffer& other) : size(other.size) {
            data = new char[size];
            std::memcpy(data, other.data, size);
        }

        // Rule of 5: Move Constructor (Efficient Ownership Transfer)
        ManagedBuffer(ManagedBuffer&& other) noexcept 
            : data(other.data), size(other.size) {
            other.data = nullptr;
            other.size = 0;
        }

        ManagedBuffer& operator=(const ManagedBuffer&) = default;
        ManagedBuffer& operator=(ManagedBuffer&&) = default;
    };
}

int main() {
    io_thecodeforge::ManagedBuffer buf1(1024);
    io_thecodeforge::ManagedBuffer buf2 = std::move(buf1); // Move constructor used
    return 0;
}
🔥Interview Gold: The Rule of Zero
The Rule of Zero: if you design your class using only standard library members (like std::unique_ptr and std::string) that manage their own resources, you don't need to write any of the five special functions. The compiler defaults will be perfectly safe. Always aim for Rule of Zero first.

Production Deployment: Dockerized C++ Performance Testing

To ensure our resource management patterns hold up under stress, we deploy our performance tests in isolated environments. This ensures that memory leaks (if any) are caught by monitoring tools without affecting the host system.

Dockerfile · DOCKERFILE
123456789101112
# Package: io.thecodeforge.infrastructure
# Multi-stage build for production-grade binary
FROM gcc:13 AS build

WORKDIR /app
COPY . .
RUN g++ -O3 -std=c++20 main.cpp -o performance_test

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y libstdc++6 && rm -rf /var/lib/apt/lists/*
COPY --from=build /app/performance_test /usr/local/bin/
CMD ["performance_test"]
▶ Output
Performance test container built successfully.
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

    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.
    Fix

    always declare virtual ~BaseClass() {} in any class you intend to be inherited from.

    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.
    Fix

    follow the Rule of Five — write a deep-copy copy constructor whenever your class owns a raw pointer.

    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.
    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 it state that the move constructor/assignment operator should be marked 'noexcept'?
  • QExplain the 'Static Type vs Dynamic Type' problem when deleting a derived object via a base pointer. How does a virtual destructor solve this?
  • QLeetCode Scenario: How would you design a LRU Cache in C++ using RAII to ensure memory for evicted nodes is cleaned up immediately without manual delete calls?
  • QWhat is 'Constructor Delegation'? In what order do the initializer lists and constructor bodies execute when one constructor calls another?
  • QWhy can't a constructor be virtual? Discuss the state of the vtable (virtual table) at the exact moment a constructor is executing.

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 are two halves of the same lifecycle contract.

Can a C++ constructor call another constructor in the same class?

Yes, this is called constructor delegation (since C++11). You invoke it in the initializer list, like MyClass() : MyClass(42) {}. This is a best practice for reducing duplicate logic across multiple parameter versions.

Why must base class destructors be marked virtual?

If you delete a derived class object through a base pointer, C++ will only call the base class destructor unless it is marked virtual. This leads to partial destruction where the derived class's members are never cleaned up, causing memory leaks and resource exhaustion.

What happens if a constructor throws an exception?

If a constructor throws, the object is considered 'never created.' Its destructor will NOT run. However, any members that were already fully constructed before the exception (via the initializer list) will have their destructors called automatically. This is why RAII members like std::string are safer than raw pointers inside a class.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

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