Skip to content
C++
Language
since C++98
Basic

Constructors and Destructors: Object Lifetime in C++

Understand how C++ constructors initialise objects and destructors clean them up — member initializer lists, constructor types, initialization order, and why destructors are the cornerstone of RAII.

By the end of this page you will write constructors with member initializer lists, understand the four main constructor types, know the exact order in which members are initialized, and write destructors that release resources reliably.

What and Why

Every C++ object has a lifetime: it is created, used, and destroyed. Constructors run at creation; destructors run at destruction. These two functions together give you a guarantee that no other major language provides: any resource you acquire in a constructor will be released when the object is destroyed — automatically, even when exceptions occur.

This guarantee is the foundation of RAII, and it is why C++ code that uses constructors and destructors correctly is more resource-safe than code that manages resources manually.

Step by Step

The default constructor

A constructor with no parameters (or all-default parameters) is the default constructor. The compiler generates one automatically if you declare no constructors at all:

cpp
struct Point {
    int x;
    int y;
};

Point p;   // default-constructed; x and y have indeterminate values for local vars

If you declare any constructor yourself, the compiler stops generating the default constructor. You can ask for it back explicitly:

cpp
class Widget {
public:
    Widget() = default;   // ask the compiler to generate it anyway
    Widget(int id) : id_(id) {}
private:
    int id_ = 0;   // default member initializer (C++11)
};

Member initializer list

The body of a constructor (the { } part) runs after all members are already initialized. If you assign in the body, you are re-assigning, not initializing — which costs an extra operation for non-trivial types.

The member initializer list — the : member(value) part after the parameter list — is where members are actually initialized:

cpp
class Connection {
    std::string host_;
    int port_;
    bool connected_;

public:
    Connection(std::string host, int port)
        : host_(std::move(host))   // initialized directly — no copy
        , port_(port)
        , connected_(false)
    {
        // body runs after all members are initialized
        open();
    }
};

For const members and reference members, a member initializer is not optional — it is mandatory:

cpp
class Config {
    const int max_connections_;   // const: cannot be assigned after init
    int& ref_;                    // reference: must be bound at construction

public:
    Config(int max, int& target)
        : max_connections_(max)   // must be here
        , ref_(target)            // must be here
    {}
};

Initialization order

The order members are initialized is always the order they are declared in the class — not the order they appear in the initializer list. Mismatches cause subtle bugs:

cpp
class Danger {
    int b_;
    int a_;   // declared after b_

public:
    // Initializer list says a_ first, then b_ — but b_ is initialized FIRST
    // because b_ is declared first. b_ = 0 (uninitialized a_) — undefined behaviour!
    Danger() : a_(5), b_(a_) {}
};

class Safe {
    int a_;
    int b_;   // declared after a_

public:
    Safe() : a_(5), b_(a_) {}   // a_ initialized first — then b_ = a_ = 5
};

The compiler will warn about this mismatch with -Wreorder.

Constructor types

Parameterised constructor — takes arguments to set initial state:

cpp
class Rectangle {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    double area() const { return width_ * height_; }
};

Rectangle r(3.0, 4.0);

Copy constructor — creates a new object as a copy of an existing one:

cpp
class Buffer {
    char* data_;
    std::size_t size_;
public:
    Buffer(const Buffer& other)       // copy constructor
        : size_(other.size_)
        , data_(new char[size_])
    {
        std::memcpy(data_, other.data_, size_);
    }
};

Buffer a(1024);
Buffer b = a;   // invokes copy constructor

Move constructor (C++11) — transfers resources from a temporary instead of copying them:

cpp
class Buffer {
    char* data_;
    std::size_t size_;
public:
    Buffer(Buffer&& other) noexcept   // move constructor
        : data_(other.data_)
        , size_(other.size_)
    {
        other.data_ = nullptr;   // leave source in valid but empty state
        other.size_ = 0;
    }
};

Buffer make_buffer();
Buffer b = make_buffer();   // move constructor — no heap copy

Delegating constructor (C++11) — one constructor calls another to avoid repeating initialization logic:

cpp
class Server {
    std::string host_;
    int port_;
    int timeout_ms_;

public:
    Server(std::string host, int port, int timeout_ms)
        : host_(std::move(host)), port_(port), timeout_ms_(timeout_ms) {}

    Server(std::string host, int port)
        : Server(std::move(host), port, 5000) {}   // delegates, sets default timeout

    Server() : Server("localhost", 8080) {}        // delegates further
};

Destructors

The destructor runs when an object's lifetime ends — when it goes out of scope, when a container destroys it, when delete is called on a pointer. It has no parameters and no return type:

cpp
class FileHandle {
    FILE* fp_;

public:
    explicit FileHandle(const char* path)
        : fp_(std::fopen(path, "r"))
    {
        if (!fp_) throw std::runtime_error("Cannot open file");
    }

    ~FileHandle() {           // destructor
        if (fp_) std::fclose(fp_);   // always runs, even if an exception is thrown
    }
};

void read_config() {
    FileHandle f("config.txt");   // constructor opens file
    // ... read from f
}   // destructor closes file here — no explicit close needed

The destructor runs in the reverse order of construction: members are destroyed in reverse declaration order, then the base class destructors run.

Virtual destructors are required in base classes when objects are deleted through a base pointer:

cpp
class Base {
public:
    virtual ~Base() = default;   // virtual! Without this, deleting a Derived* via Base* is UB
};

class Derived : public Base {
    std::string data_;
public:
    ~Derived() override = default;   // compiler generates cleanup of data_
};

Base* obj = new Derived();
delete obj;   // calls ~Derived(), then ~Base() — correct because ~Base() is virtual

Common Patterns

Resource acquisition in constructor, release in destructor

cpp
class MutexLock {
    std::mutex& mu_;
public:
    explicit MutexLock(std::mutex& mu) : mu_(mu) { mu_.lock(); }
    ~MutexLock() { mu_.unlock(); }

    // Prevent copying — a lock cannot be duplicated
    MutexLock(const MutexLock&) = delete;
    MutexLock& operator=(const MutexLock&) = delete;
};

void critical_section(std::mutex& mu) {
    MutexLock lock(mu);   // mutex locked here
    // ... safe to access shared data
}   // lock destroyed here — mutex released automatically

Rule of Three / Five

If you write a destructor that frees a resource, you almost certainly also need a copy constructor and copy assignment operator — or you should delete them. See Rule of Five.

What Can Go Wrong

Initialization order surprise

cpp
class Widget {
    int size_;
    std::vector<int> data_;   // declared after size_

public:
    Widget(int n) : data_(n), size_(data_.size()) {}
    // BUG: size_ is initialized BEFORE data_ (declaration order).
    // size_ = data_.size() where data_ is not yet constructed — UB.
};

Fix: reorder declarations, or derive size_ from n directly.

Forgetting noexcept on move constructors

Without noexcept, std::vector cannot use the move constructor during reallocation — it falls back to copying. Always mark move constructors (and move assignment) noexcept when they genuinely cannot throw.

Throwing in a destructor

An exception escaping a destructor during stack unwinding (another exception already in flight) calls std::terminate. Destructors should never throw:

cpp
~Connection() {
    try {
        socket_.close();   // might throw — catch it!
    } catch (...) {
        // log the error, but do not propagate
    }
}

Quick Reference

WhatSyntax
Default constructorWidget() = default;
Parameterised constructorWidget(int x) : x_(x) {}
Member initializer list: member_(val), other_(val2)
Copy constructorWidget(const Widget&)
Move constructorWidget(Widget&&) noexcept
Delegating constructorWidget() : Widget(0) {} (C++11)
Destructor~Widget() { /* cleanup */ }
Virtual destructorvirtual ~Base() = default;
Suppress defaultWidget() = delete;

Destructor Order Guarantee

Destruction always happens in the exact reverse order of construction. This means members are destroyed in reverse declaration order, and in an inheritance hierarchy the most-derived class destructor runs first, working back up to the base class destructor last. The symmetry is intentional: whatever you acquired first, you release last.

cpp
#include <iostream>

struct Base {
    Base()  { std::cout << "Base constructed\n"; }
    ~Base() { std::cout << "Base destroyed\n"; }
};

struct Member {
    std::string name;
    Member(std::string n) : name(n) { std::cout << name << " constructed\n"; }
    ~Member() { std::cout << name << " destroyed\n"; }
};

struct Derived : Base {
    Member first_{"first"};
    Member second_{"second"};

    Derived()  { std::cout << "Derived constructed\n"; }
    ~Derived() { std::cout << "Derived destroyed\n"; }
};

int main() {
    Derived d;
}
// Output:
// Base constructed
// first constructed
// second constructed
// Derived constructed
// Derived destroyed
// second destroyed      <- reverse of construction
// first destroyed
// Base destroyed        <- base last

This guarantee makes RAII composable: a class holding two RAII members does not need to do anything special — their destructors fire in the right order automatically.

Virtual Destructors

If a class has any virtual method, its destructor must also be virtual. When you delete a derived object through a base-class pointer and the destructor is not virtual, the compiler only calls the base destructor — the derived destructor never runs, leaking whatever the derived class owned.

cpp
// WRONG — base destructor is not virtual
class Animal {
public:
    virtual void speak() = 0;
    ~Animal() { std::cout << "Animal destroyed\n"; }  // non-virtual!
};

class Dog : public Animal {
    std::string name_;
public:
    Dog(std::string n) : name_(std::move(n)) {}
    void speak() override { std::cout << "Woof\n"; }
    ~Dog() { std::cout << "Dog " << name_ << " destroyed\n"; }
};

Animal* a = new Dog("Rex");
delete a;   // UNDEFINED BEHAVIOUR — only ~Animal() runs, ~Dog() is skipped
cpp
// CORRECT — add virtual to the base destructor
class Animal {
public:
    virtual void speak() = 0;
    virtual ~Animal() = default;   // one keyword fixes everything
};

class Dog : public Animal {
    std::string name_;
public:
    Dog(std::string n) : name_(std::move(n)) {}
    void speak() override { std::cout << "Woof\n"; }
    ~Dog() override = default;   // 'override' is optional but documents intent
};

Animal* a = new Dog("Rex");
delete a;   // correct: ~Dog() runs first, then ~Animal()

A useful rule of thumb: if a class is meant to be inherited from (has any virtual method), declare virtual ~Base() = default; immediately. If a class is not meant to be polymorphic, mark it final or leave the destructor non-virtual.

noexcept on Destructors

Destructors are implicitly noexcept in C++11 and later — the compiler adds it for you. This is intentional: an exception escaping a destructor during stack unwinding (while another exception is already propagating) calls std::terminate immediately. There is no safe way to recover.

cpp
class Resource {
    int* data_;
public:
    explicit Resource(int n) : data_(new int[n]) {}

    // Implicitly noexcept — the compiler infers this
    ~Resource() {
        delete[] data_;
    }
};

Because noexcept is implicit, the only time you need to write it yourself is when the compiler cannot infer it — typically when the destructor calls a function whose noexcept status is unknown. In that case, declare it explicitly:

cpp
~MyClass() noexcept {
    try {
        flush_cache();   // might throw internally
    } catch (...) {
        // swallow — never let exceptions escape
    }
}

The general rule is: catch all exceptions inside the destructor body and handle or log them. Never let them escape.

Rule of Five in Practice

If you manage a raw resource in a class, you need all five special member functions: destructor, copy constructor, copy assignment, move constructor, and move assignment. The "Rule of Five" codifies this. Here is a complete, correct RAII class that wraps a heap-allocated array:

cpp
#include <algorithm>   // std::copy
#include <cstddef>     // std::size_t
#include <utility>     // std::exchange

class IntArray {
    int*        data_;
    std::size_t size_;

public:
    // 1. Constructor
    explicit IntArray(std::size_t n)
        : data_(n ? new int[n]() : nullptr), size_(n) {}

    // 2. Destructor
    ~IntArray() {
        delete[] data_;
    }

    // 3. Copy constructor — deep copy
    IntArray(const IntArray& other)
        : data_(other.size_ ? new int[other.size_] : nullptr)
        , size_(other.size_)
    {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    // 4. Copy assignment — copy-and-swap idiom
    IntArray& operator=(IntArray other) noexcept {
        swap(other);   // other takes our old resources, cleans up in its dtor
        return *this;
    }

    // 5. Move constructor — steal resources, leave source empty
    IntArray(IntArray&& other) noexcept
        : data_(std::exchange(other.data_, nullptr))
        , size_(std::exchange(other.size_, 0))
    {}

    // Move assignment is handled by the copy-assignment operator above
    // (because it accepts by value, move-constructing the argument when called with an rvalue)

    void swap(IntArray& other) noexcept {
        std::swap(data_, other.data_);
        std::swap(size_, other.size_);
    }

    std::size_t size() const noexcept { return size_; }
    int& operator[](std::size_t i) { return data_[i]; }
    const int& operator[](std::size_t i) const { return data_[i]; }
};

Notice that std::exchange is a clean way to move a value out and replace it with a safe sentinel in one expression. After a move, the source object must be in a valid but unspecified state — nullptr/0 satisfies that requirement for a raw pointer array.

Aggregate Initialization vs. Constructor

When a type has no user-provided constructors, no private or protected non-static data members, and no virtual functions, it is an aggregate — and brace initialization bypasses constructors entirely. Understanding the distinction prevents surprises.

cpp
// Aggregate — no constructor declared
struct Point {
    int x;
    int y;
};

Point p1{3, 4};    // aggregate initialization — directly sets x=3, y=4
Point p2 = {5, 6}; // also aggregate initialization
cpp
// NOT an aggregate — has a user-provided constructor
class Pixel {
    int x_, y_;
    int color_;
public:
    Pixel(int x, int y) : x_(x), y_(y), color_(0) {}
};

Pixel px{10, 20};   // calls Pixel(int, int) constructor — not aggregate init

In C++20, a class with a user-declared constructor (even = default) is no longer an aggregate. This tightened the rules to reduce surprising behavior:

cpp
struct C20Example {
    int value;
    C20Example() = default;   // user-declared, not user-provided
};

// C++17: aggregate initialization still works — C20Example{42} is valid
// C++20: NOT an aggregate — C20Example{42} is an error (no matching constructor)

When in doubt: if you want aggregate-style brace initialization, avoid declaring any constructor. If you need invariants, write a constructor and document that the type is not an aggregate.

Explicit Destructor Calls and Placement New

You can construct an object into memory you manage yourself using placement new, and you must then call the destructor explicitly when you are done. This is an advanced pattern used inside allocators, containers, and std::optional-like types — beginners rarely need it, but knowing it exists removes the mystery from library internals.

cpp
#include <new>       // std::launder (C++17)
#include <cstdlib>   // std::aligned_alloc / std::free

// Allocate raw, aligned storage
void* raw = std::aligned_alloc(alignof(std::string), sizeof(std::string));

// Construct a std::string in that storage using placement new
std::string* s = new (raw) std::string("hello");

// Use the object normally
(*s) += " world";

// Explicit destructor call — required because no delete[] will run here
s->~basic_string();   // or: s->~std::string();

// Release the raw memory (do NOT use 'delete s' — that would double-free)
std::free(raw);

The explicit destructor call s->~basic_string() is the only correct way to end the object's lifetime without freeing the memory underneath it. Omitting it leaks whatever the object owns. Using delete s instead would also attempt to free raw a second time — undefined behaviour.

In everyday code, prefer std::optional, std::variant, or a custom allocator-aware container rather than managing placement new yourself.

Common Mistakes

Two mistakes recur so often in C++ codebases that they deserve a dedicated checklist.

Non-virtual destructor in a polymorphic base class

Inheriting from a class without a virtual destructor and then deleting through a base pointer is undefined behaviour. Static analysis tools (clang-tidy check cppcoreguidelines-virtual-class-destructor) catch this automatically. The fix is one keyword:

cpp
// Before — silent UB when deleting via base pointer
class Shape {
public:
    virtual double area() = 0;
    ~Shape() {}   // BUG: non-virtual
};

// After — correct
class Shape {
public:
    virtual double area() = 0;
    virtual ~Shape() = default;   // FIXED
};

Exception escaping a destructor

An exception that escapes a destructor while another exception is already unwinding the stack causes std::terminate. The pattern to avoid it is always the same — wrap everything in try/catch(...):

cpp
class NetworkConnection {
    Socket socket_;
public:
    ~NetworkConnection() {
        try {
            socket_.shutdown();  // can throw if already disconnected
            socket_.close();
        } catch (const std::exception& e) {
            log_error("shutdown failed: ", e.what());
        } catch (...) {
            log_error("unknown error during shutdown");
        }
        // execution reaches here cleanly — destructor exits normally
    }
};

Logging is fine inside a catch block in a destructor; re-throwing is never fine.

What's Next

  • RAII Introduction — how constructors and destructors eliminate manual resource management entirely.
  • Classes Basics — the full picture of writing C++ classes.
  • Rule of Five — when to write (or delete) all five special member functions.