Last updated: December 2025

C++ Constructors and Destructors: All Types, Initializer Lists, Move Semantics & DSA (2025)

By CoodeVerse Editorial Team ✓ 2025 Verified ⏱ 20 min read 🎯 Beginner–Intermediate 📦 C++11/17
Difficulty:
Beginner–Intermediate — Prerequisites: Classes & Objects

⚡ Quick Answer: Constructors & Destructors in C++

Constructors and destructors are C++'s mechanism for guaranteeing that objects always start in a valid state and always clean up after themselves — no matter how the code exits, even through exceptions. This guide covers every constructor type with deep explanations, the shallow vs deep copy problem visualized, move semantics, RAII, virtual destructors, and four complete DSA examples.

🔄

Object Lifecycle

Birth → life → death

⚙️

4 Constructor Types

Default, param, copy, move

📋

Initializer Lists

Why they matter

📑

Shallow vs Deep Copy

The double-free bug

🚀

Move Semantics

noexcept & std::move

🧹

Destructors & RAII

Virtual + cleanup

🏆

DSA Examples

Queue, tree, graph

FAQ

Interview questions

🔄 Section 1

Object Lifecycle — Birth to Destruction

Every C++ object goes through a defined lifecycle. Understanding it is essential for writing correct code with resources like heap memory:

C++ Object Lifecycle
new / scope entry Constructor runs Object in use Scope ends / delete called Destructor runs Memory freed
Stack objects: constructor runs at declaration, destructor at closing brace. Heap objects: constructor runs at new, destructor at delete.
RAII in one sentence: Tie a resource's lifetime to an object's lifetime — the constructor acquires the resource, the destructor releases it. Since destructors run automatically (even when exceptions are thrown), this guarantees the resource is always released. This is the foundation of modern C++ resource management.
⚙️ Section 2

All 4 Constructor Types With Examples

1. Default constructor
No parameters. Called by MyClass obj;. Auto-generated if you define no constructors. Must explicitly write MyClass() = default; to get it back after defining other constructors.
2. Parameterized constructor
Takes arguments. Called by MyClass obj(x); or MyClass obj{x};. Use initializer list: MyClass(int x) : val(x) {}. Can be overloaded.
3. Copy constructor
Creates from existing object. MyClass(const MyClass& o). Called by MyClass b = a; and when passing by value. Compiler-generated version does shallow copy.
4. Move constructor (C++11)
Transfers resources from temporary. MyClass(MyClass&& o) noexcept. Mark noexcept — std::vector uses move only if noexcept. Called by MyClass b = std::move(a);.
all_constructors.cpp — one class showing all 4 typesC++
#include <iostream>
#include <string>
#include <utility>    // std::move
using namespace std;

class Student {
private:
    string name;
    int    id;
    int*   scores;   // heap resource

public:
    // 1. Default constructor
    Student() : name("Unknown"), id(0), scores(new int[3]()) {
        cout << "Default ctor\n";
    }

    // 2. Parameterized constructor
    Student(string n, int i) : name(move(n)), id(i), scores(new int[3]{90,85,88}) {
        cout << "Param ctor: " << name << "\n";
    }

    // 3. Copy constructor — DEEP copy
    Student(const Student& o)
        : name(o.name), id(o.id), scores(new int[3]) {
        for (int i=0; i<3; i++) scores[i] = o.scores[i];
        cout << "Copy ctor: " << name << "\n";
    }

    // 4. Move constructor — transfer, no copy
    Student(Student&& o) noexcept
        : name(move(o.name)), id(o.id), scores(o.scores) {
        o.scores = nullptr;  // leave moved-from object safe to destruct
        o.id     = 0;
        cout << "Move ctor: " << name << "\n";
    }

    // Destructor
    ~Student() {
        delete[] scores;   // safe: delete[] nullptr is a no-op
        cout << "Dtor: " << name << "\n";
    }

    void display() const {
        cout << name << " (id=" << id << "): ";
        if (scores) cout << scores[0] << " " << scores[1] << " " << scores[2];
        cout << "\n";
    }
};

int main() {
    Student s1;                         // default
    Student s2("Alice", 101);           // parameterized
    Student s3 = s2;                     // copy (deep)
    Student s4 = move(s2);              // move (s2 now empty)

    s3.display();
    s4.display();
    s2.display();
    return 0;
}
Output
Default ctor
Param ctor: Alice
Copy ctor: Alice
Move ctor: Alice
Alice (id=101): 90 85 88
Alice (id=101): 90 85 88
(id=0):
Dtor: Alice
Dtor: Alice
Dtor: Unknown
Dtor:
📋 Section 3

Initializer Lists — Why They're Required and Preferred

An initializer list initializes member variables before the constructor body runs. Syntax: ClassName(params) : member1(val1), member2(val2) { body }

ScenarioBody assignmentInitializer list
const member✗ Compile error✓ Required
Reference member✗ Compile error✓ Required
Member with no default ctor✗ Compile error✓ Required
Built-in types (int, double)⚠ 2 steps (init + assign)✓ 1 step (direct init)
std::string member⚠ Default ctor + assignment✓ Construct directly
initializer_list_demo.cppC++
class Circle {
    const double PI;
    double&       area;
    double        r;

public:
    Circle(double& areaRef, double radius)
        : PI(3.14159),
          area(areaRef),
          r(radius)
    {
        area = PI * r * r;
    }
    double getR() const { return r; }
};
Initialization order follows declaration order, not list order. Always list members in their declaration order to avoid subtle bugs. GCC warns about this with -Wall.
📑 Section 4

Shallow vs Deep Copy — The Double-Free Bug Explained

When a class has a raw pointer member, the compiler-generated copy constructor does a shallow copy — it copies the pointer value, not the data it points to. Both objects now share the same heap memory. The first destructor frees it; the second crashes with a double-free.

❌ Shallow copy (default)
obj1.data[1,2,3] @0x100
obj2.data[1,2,3] @0x100
Both point to the SAME memory.
First ~dtor frees it. Second ~dtor crashes: double-free!
✅ Deep copy (correct)
obj1.data[1,2,3] @0x100
obj2.data[1,2,3] @0x200
Each object owns its OWN copy.
Each ~dtor frees its own memory safely. ✓
deep_copy.cppC++
#include <iostream>
#include <cstring>
using namespace std;

class Buffer {
    int* data;
    int  n;
public:
    Buffer(int size) : n(size), data(new int[size]()) {}

    Buffer(const Buffer& o) : n(o.n), data(new int[o.n]) {
        memcpy(data, o.data, n * sizeof(int));
        cout << "Deep copy of " << n << " ints\n";
    }

    Buffer& operator=(const Buffer& o) {
        if (this == &o) return *this;
        delete[] data;
        n = o.n; data = new int[n];
        memcpy(data, o.data, n * sizeof(int));
        return *this;
    }

    ~Buffer() { delete[] data; }
    int& operator[](int i) { return data[i]; }
    int  size() const { return n; }
};

int main() {
    Buffer a(3); a[0]=1; a[1]=2; a[2]=3;
    Buffer b = a;
    b[0] = 99;
    cout << a[0] << " " << b[0] << endl;  // 1 99
    return 0;
}
Output
Deep copy of 3 ints
1 99
🚀 Section 5

Move Constructors & noexcept (C++11)

A move constructor transfers ownership of resources from a temporary object rather than copying them. This is O(1) instead of O(n) for large data structures.

move_constructor.cppC++
#include <iostream>
#include <utility>
using namespace std;

class BigArray {
    int* data;
    int  n;
public:
    BigArray(int size) : n(size), data(new int[size]()) {
        cout << "Allocated " << n << " ints\n";
    }
    BigArray(const BigArray& o) : n(o.n), data(new int[o.n]) {
        for (int i=0; i<n; i++) data[i] = o.data[i];
        cout << "Copied " << n << " ints (slow)\n";
    }
    BigArray(BigArray&& o) noexcept : n(o.n), data(o.data) {
        o.data = nullptr;
        o.n    = 0;
        cout << "Moved (O(1))\n";
    }
    ~BigArray() { delete[] data; }
    int size() const { return n; }
};

int main() {
    BigArray a(1000000);
    BigArray b = a;
    BigArray c = move(a);
    cout << "a.size()=" << a.size() << " c.size()=" << c.size() << endl;
    return 0;
}
Output
Allocated 1000000 ints
Copied 1000000 ints (slow)
Moved (O(1))
a.size()=0 c.size()=1000000
Why noexcept on move constructors? std::vector only uses move operations during reallocation if the move constructor is noexcept. Without it, std::vector falls back to the copy constructor. Always mark move constructors (and move assignment) noexcept unless they genuinely can throw.
🧹 Section 6

Destructors, Virtual Destructors & RAII

virtual_destructor.cppC++
#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() { cout << "Base dtor\n"; }
    virtual void show() { cout << "Base\n"; }
};

class Derived : public Base {
    int* resource;
public:
    Derived() : resource(new int(42)) { cout << "Derived ctor\n"; }
    ~Derived() {
        delete resource;
        cout << "Derived dtor (freed resource)\n";
    }
    void show() override { cout << "Derived\n"; }
};

int main() {
    Base* p = new Derived();
    p->show();
    delete p;
    return 0;
}
Output
Derived ctor
Derived
Derived dtor (freed resource)
Base dtor

RAII in practice — automatic file handle management

raii_file.cppC++
#include <fstream>
#include <stdexcept>
#include <iostream>
using namespace std;

class ManagedFile {
    ofstream file;
public:
    ManagedFile(const string& path) {
        file.open(path);
        if (!file) throw runtime_error("Cannot open: " + path);
        cout << "File opened\n";
    }
    ~ManagedFile() {
        file.close();
        cout << "File closed (guaranteed by RAII)\n";
    }
    void write(const string& s) { file << s; }
};

void process() {
    ManagedFile f("out.txt");
    f.write("hello\n");
    throw runtime_error("Something went wrong");
}

int main() {
    try { process(); }
    catch (const exception& e) { cout << "Caught: " << e.what() << endl; }
    return 0;
}
Output
File opened
File closed (guaranteed by RAII)
Caught: Something went wrong
➕ Section 7

explicit, =default, =delete & Delegating Constructors

modern_ctor_features.cppC++11
class Radius {
public:
    explicit Radius(double r) : val(r) {}
    double val;
};

class Shape {
    double x, y, size;
public:
    Shape(double x, double y, double s) : x(x), y(y), size(s) {}
    Shape() : Shape(0, 0, 1) {}
    Shape(double s) : Shape(0, 0, s) {}
};

class Singleton {
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton() = default;
};
🏆 Section 8

DSA Applications: Queue, Tree Node, Graph

1. Circular Queue with constructor/destructor

circular_queue.cppC++
#include <iostream>
#include <stdexcept>
using namespace std;

class CircularQueue {
    int* arr;
    int  cap, front, rear, sz;
public:
    explicit CircularQueue(int capacity)
        : cap(capacity), front(0), rear(-1), sz(0),
          arr(new int[capacity]) {
        cout << "Queue(cap=" << cap << ")\n";
    }
    CircularQueue(const CircularQueue& o)
        : cap(o.cap), front(o.front), rear(o.rear), sz(o.sz),
          arr(new int[o.cap]) {
        for (int i=0; i<cap; i++) arr[i] = o.arr[i];
        cout << "Queue copied\n";
    }
    ~CircularQueue() { delete[] arr; cout << "Queue destroyed\n"; }
    void enqueue(int v) {
        if (sz == cap) throw overflow_error("Queue full");
        rear = (rear + 1) % cap; arr[rear] = v; sz++;
    }
    int dequeue() {
        if (sz == 0) throw underflow_error("Queue empty");
        int v = arr[front]; front = (front + 1) % cap; sz--;
        return v;
    }
    int  size()    const { return sz; }
    bool isEmpty() const { return sz == 0; }
};

int main() {
    CircularQueue q(3);
    q.enqueue(1); q.enqueue(2); q.enqueue(3);
    cout << q.dequeue() << endl;
    CircularQueue q2 = q;
    cout << q2.dequeue() << endl;
    return 0;
}
Output
Queue(cap=3)
1
Queue copied
2
Queue destroyed
Queue destroyed

2. Binary Tree with RAII node management

binary_tree.cppC++
#include <iostream>
#include <memory>
using namespace std;

struct BSTNode {
    int val;
    unique_ptr<BSTNode> left, right;
    explicit BSTNode(int v) : val(v) {}
};

class BST {
    unique_ptr<BSTNode> root;
    void insert_(BSTNode*& node, int v) {
        if (!node) { node = new BSTNode(v); return; }
        if (v < node->val) insert_((BSTNode*&)node->left, v);
        else               insert_((BSTNode*&)node->right, v);
    }
    void inorder_(const BSTNode* n) const {
        if (!n) return;
        inorder_(n->left.get());
        cout << n->val << " ";
        inorder_(n->right.get());
    }
public:
    void insert(int v) { insert_((BSTNode*&)root, v); }
    void inorder() const { inorder_(root.get()); cout << endl; }
};

int main() {
    BST t;
    for (int x : {5,2,7,1,3}) t.insert(x);
    t.inorder();
    return 0;
}
Output
1 2 3 5 7

Best Practices & Common Mistakes

✅ Use initializer lists

Required for const/reference members; preferred for everything. Initializes in one step instead of two.

✅ Deep copy for raw pointers

Any class with int* ptr needs a hand-written copy constructor, copy assignment, and destructor (Rule of Three).

✅ Mark move ctor noexcept

std::vector only moves during reallocation if the move constructor is noexcept.

✅ Virtual destructor in base classes

Any class with a virtual method needs virtual ~Base() = default; to avoid derived resource leaks.

✅ Use RAII

Acquire in constructor, release in destructor. Resources are freed even if exceptions are thrown.

✅ Prefer unique_ptr over raw new

unique_ptr implements RAII automatically — no manual destructor, no memory leaks, no Rule of Three needed.

✅ Mark single-arg ctors explicit

Prevents accidental implicit conversion: explicit MyClass(int x).

❌ delete[] for arrays

Pair new T[n] with delete[], not delete. Mismatching them is undefined behavior.

FAQ / Interview Questions

A constructor (ClassName()) initializes an object when it's created — sets up member variables and acquires resources. A destructor (~ClassName()) cleans up when an object is destroyed — releases resources. Constructor: called once, can be overloaded, can have parameters. Destructor: called once, cannot be overloaded, no parameters, cannot have a return type.
Shallow copy copies the value of a pointer member — both objects share the same heap memory. When the first destructor frees it, the second destructor crashes with a double-free. Deep copy allocates new heap memory and copies the data — each object has an independent copy. The compiler-generated copy constructor does a shallow copy. Define a deep-copy constructor for any class with raw pointer members.
std::vector only uses move operations during reallocation when the move constructor is noexcept. Without noexcept, vector falls back to copying all elements during reallocation, which is O(n) instead of O(1). Mark all move constructors and move assignment operators noexcept unless they genuinely might throw.
RAII (Resource Acquisition Is Initialization) ties a resource's lifetime to an object's lifetime. The constructor acquires the resource; the destructor releases it. Since C++ guarantees destructors run when objects leave scope — even through exceptions — RAII guarantees resource cleanup with no leaks and no explicit cleanup code. Examples: std::fstream (file), std::lock_guard (mutex), std::unique_ptr (heap memory).
Construction order: base class constructor → member constructors (in declaration order) → derived class constructor body. Destruction order: exact reverse — derived class destructor body → member destructors (reverse declaration order) → base class destructor. For local variables: constructed in declaration order, destroyed in reverse.
No. A class can have exactly one destructor — it has no parameters and no return type, so there's nothing to overload on. You can have multiple overloaded constructors (differing in parameter types and counts), but only one destructor.

Related C++ Topics on CoodeVerse

Classes & Objects Inheritance & Polymorphism Advanced OOP Smart Pointers Move Semantics & RAII Arrays & Strings Templates 📚 Full C++ Course

CoodeVerse Editorial Team

Senior engineers and CS educators. All code tested with GCC 13 and Clang 16, updated to C++17.