C++ Constructors and Destructors: All Types, Initializer Lists, Move Semantics & DSA (2025)
⚡ Quick Answer: Constructors & Destructors in C++
- Constructor — same name as class, no return type, called on object creation
- 4 types: default, parameterized, copy (deep/shallow), move (C++11)
- Destructor —
~ClassName(), called on destruction, release resources - Initializer list —
: member(value), required for const/reference, preferred always - noexcept — mark move constructors noexcept for std::vector performance
- Virtual destructor — mandatory in polymorphic base classes
- RAII — constructor acquires, destructor releases = exception-safe resource management
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
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:
All 4 Constructor Types With Examples
MyClass obj;. Auto-generated if you define no constructors. Must explicitly write MyClass() = default; to get it back after defining other constructors.MyClass obj(x); or MyClass obj{x};. Use initializer list: MyClass(int x) : val(x) {}. Can be overloaded.MyClass(const MyClass& o). Called by MyClass b = a; and when passing by value. Compiler-generated version does shallow copy.MyClass(MyClass&& o) noexcept. Mark noexcept — std::vector uses move only if noexcept. Called by MyClass b = std::move(a);.#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;
}
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: 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 }
| Scenario | Body assignment | Initializer 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 |
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; }
};
-Wall.
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.
First ~dtor frees it. Second ~dtor crashes: double-free!
Each ~dtor frees its own memory safely. ✓
#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;
}
Deep copy of 3 ints
1 99Move 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.
#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;
}
Allocated 1000000 ints
Copied 1000000 ints (slow)
Moved (O(1))
a.size()=0 c.size()=1000000std::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.
Destructors, Virtual Destructors & RAII
#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;
}
Derived ctor
Derived
Derived dtor (freed resource)
Base dtorRAII in practice — automatic file handle management
#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;
}
File opened
File closed (guaranteed by RAII)
Caught: Something went wrongexplicit, =default, =delete & Delegating Constructors
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;
};
DSA Applications: Queue, Tree Node, Graph
1. Circular Queue with constructor/destructor
#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;
}
Queue(cap=3)
1
Queue copied
2
Queue destroyed
Queue destroyed2. Binary Tree with RAII node management
#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;
}
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
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.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.std::fstream (file), std::lock_guard (mutex), std::unique_ptr (heap memory).Master C++ from basics to advanced OOP
Structured lessons, 200+ exercises, certificate. Join 50,000+ students on CoodeVerse.