What Is a Constructor?
A constructor is a special member function that is automatically called when an object is created. Its job is to put the object into a well-defined initial state. Constructors have the same name as the class and no return type — not even void.
class Student {
public:
Student() { // constructor — same name, no return type
cout << "Student object created!\n";
}
};
int main() {
Student s; // constructor is called automatically here
return 0;
}
Understanding constructors deeply is a requirement for C++ interviews at companies like Google India, Microsoft, and Adobe. The "Rule of Three" and "Rule of Five" (which follow from this chapter) appear in almost every senior C++ interview.
Default Constructor
A default constructor takes no arguments. If you define no constructor at all, the compiler generates a default constructor for you. The moment you define any constructor, the compiler no longer generates the default one automatically.
class Box {
public:
double width, height, depth;
// User-defined default constructor
Box() {
width = 1.0;
height = 1.0;
depth = 1.0;
cout << "Default Box (1x1x1) created\n";
}
};
int main() {
Box b; // calls the default constructor
return 0;
}
Parameterised Constructor
A parameterised constructor accepts arguments, allowing each object to be initialised with different data at creation time.
class Box {
public:
double width, height, depth;
Box(double w, double h, double d) {
width = w;
height = h;
depth = d;
}
double volume() const {
return width * height * depth;
}
};
int main() {
Box b1(2.0, 3.0, 4.0);
Box b2(5.0, 5.0, 5.0);
cout << "Volume of b1: " << b1.volume() << "\n"; // 24
cout << "Volume of b2: " << b2.volume() << "\n"; // 125
return 0;
}
Constructor Initialiser List
Rather than assigning member variables inside the constructor body, you can initialise them in the initialiser list — the section between : and {. This is more efficient because it initialises members directly instead of default-constructing then assigning them. For const members and references, the initialiser list is the only option.
class Student {
private:
string name;
int rollNumber;
const int batchYear; // const — MUST use initialiser list
public:
// Initialiser list syntax
Student(string n, int roll, int year)
: name(n), rollNumber(roll), batchYear(year)
{
cout << "Student " << name << " created.\n";
}
void display() const {
cout << name << " | Roll: " << rollNumber
<< " | Batch: " << batchYear << "\n";
}
};
Members are initialised in the order they are declared in the class body, regardless of the order in the initialiser list. Keeping both orders consistent avoids subtle bugs.
Copy Constructor
A copy constructor is called when a new object is created as a copy of an existing one. The compiler generates a shallow (member-wise) copy constructor automatically, but when your class owns a raw pointer to heap memory, you must write a deep copy constructor yourself.
class MyArray {
private:
int* data;
int size;
public:
// Parameterised constructor
MyArray(int s) : size(s) {
data = new int[size];
for (int i = 0; i < size; i++) data[i] = i * 10;
cout << "Constructed: size " << size << "\n";
}
// Deep copy constructor
MyArray(const MyArray& other) : size(other.size) {
data = new int[size]; // allocate NEW memory
for (int i = 0; i < size; i++) {
data[i] = other.data[i]; // copy the values
}
cout << "Deep-copied: size " << size << "\n";
}
void print() const {
for (int i = 0; i < size; i++) cout << data[i] << " ";
cout << "\n";
}
~MyArray() {
delete[] data;
cout << "Destroyed: size " << size << "\n";
}
};
int main() {
MyArray a(4);
MyArray b = a; // copy constructor called
b.print(); // 0 10 20 30
return 0;
}
Without a deep copy constructor, b would share a's memory. When one is destroyed, the other's data becomes invalid — a double-free bug.
Move Constructor (C++11)
A move constructor "steals" the resources of a temporary (rvalue) object instead of copying them — dramatically cheaper for large objects. It is identified by the rvalue reference parameter &&.
class MyArray {
private:
int* data;
int size;
public:
MyArray(int s) : size(s), data(new int[s]) {
for (int i = 0; i < s; i++) data[i] = i;
cout << "Constructed\n";
}
// Move constructor
MyArray(MyArray&& other) noexcept
: data(other.data), size(other.size)
{
other.data = nullptr; // leave 'other' in a valid but empty state
other.size = 0;
cout << "Moved\n";
}
~MyArray() {
delete[] data; // safe: deleting nullptr is a no-op
}
};
MyArray makeArray(int n) {
return MyArray(n); // return value triggers move, not copy
}
int main() {
MyArray arr = makeArray(5); // Constructed + (possibly) Moved
return 0;
}
noexcept on the move constructor is important — the STL will only use move operations if they are marked noexcept.
Delegating Constructors (C++11)
A delegating constructor calls another constructor of the same class in its initialiser list. This avoids duplicating initialisation logic.
class Connection {
private:
string host;
int port;
bool secure;
public:
// Primary constructor
Connection(string h, int p, bool s)
: host(h), port(p), secure(s) {}
// Delegating constructors
Connection(string h, int p)
: Connection(h, p, false) {} // delegates to primary
Connection(string h)
: Connection(h, 80) {} // delegates to second
void show() const {
cout << (secure ? "https" : "http") << "://"
<< host << ":" << port << "\n";
}
};
int main() {
Connection c1("api.meritshot.com", 443, true);
Connection c2("api.meritshot.com", 8080);
Connection c3("api.meritshot.com");
c1.show(); // https://api.meritshot.com:443
c2.show(); // http://api.meritshot.com:8080
c3.show(); // http://api.meritshot.com:80
return 0;
}
The Destructor
A destructor is called automatically when an object goes out of scope or is explicitly deleted. It has the same name as the class, prefixed with ~, takes no arguments, and returns nothing.
class FileHandle {
private:
string filename;
bool isOpen;
public:
FileHandle(string name) : filename(name), isOpen(true) {
cout << "Opened: " << filename << "\n";
}
~FileHandle() {
if (isOpen) {
isOpen = false;
cout << "Closed: " << filename << "\n";
}
}
};
int main() {
{
FileHandle f("report.csv");
// ... use f ...
} // f goes out of scope here — destructor called automatically
cout << "After block\n";
return 0;
}
Output:
Opened: report.csv
Closed: report.csv
After block
RAII — Resource Acquisition Is Initialisation
RAII is the most important resource-management idiom in C++. The principle: acquire a resource in the constructor, release it in the destructor. Because the destructor is called deterministically when an object goes out of scope, resources are never leaked — not even when exceptions occur.
Resources managed via RAII:
- Heap memory (
new/delete) - File handles (
fopen/fclose) - Network sockets
- Database connections
- Mutex locks
The standard library's unique_ptr, shared_ptr, fstream, lock_guard, and vector all follow RAII. When you write your own classes that own resources, follow the same pattern.
Worked Example: A Student Class with All Constructor Types
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
string name;
int rollNumber;
double* cgpaHistory; // heap-allocated array of CGPA per semester
int semesters;
static int totalStudents;
public:
// 1. Default constructor
Student()
: name("Unknown"), rollNumber(0),
cgpaHistory(nullptr), semesters(0)
{
totalStudents++;
cout << "[Default] Student created: " << name << "\n";
}
// 2. Parameterised constructor
Student(string n, int roll, int sem)
: name(n), rollNumber(roll), semesters(sem)
{
cgpaHistory = new double[semesters](); // zero-initialised
totalStudents++;
cout << "[Param] Student created: " << name << "\n";
}
// 3. Copy constructor (deep copy)
Student(const Student& other)
: name(other.name),
rollNumber(other.rollNumber),
semesters(other.semesters)
{
cgpaHistory = new double[semesters];
for (int i = 0; i < semesters; i++) {
cgpaHistory[i] = other.cgpaHistory[i];
}
totalStudents++;
cout << "[Copy] Copied student: " << name << "\n";
}
// 4. Move constructor (C++11)
Student(Student&& other) noexcept
: name(move(other.name)),
rollNumber(other.rollNumber),
cgpaHistory(other.cgpaHistory),
semesters(other.semesters)
{
other.cgpaHistory = nullptr;
other.semesters = 0;
other.rollNumber = 0;
totalStudents++;
cout << "[Move] Moved student: " << name << "\n";
}
// Set CGPA for a semester (1-based)
void setSemesterCGPA(int sem, double cgpa) {
if (sem >= 1 && sem <= semesters) {
cgpaHistory[sem - 1] = cgpa;
}
}
double averageCGPA() const {
if (semesters == 0 || cgpaHistory == nullptr) return 0.0;
double sum = 0.0;
for (int i = 0; i < semesters; i++) sum += cgpaHistory[i];
return sum / semesters;
}
void display() const {
cout << "Name: " << name
<< ", Roll: " << rollNumber
<< ", Avg CGPA: " << averageCGPA() << "\n";
}
static int getTotalStudents() { return totalStudents; }
// Destructor — release heap memory (RAII)
~Student() {
delete[] cgpaHistory;
totalStudents--;
cout << "[Destruct] Student destroyed: " << name << "\n";
}
};
int Student::totalStudents = 0;
int main() {
cout << "=== Default constructor ===\n";
Student s0;
cout << "\n=== Parameterised constructor ===\n";
Student s1("Aditya Kumar", 101, 4);
s1.setSemesterCGPA(1, 8.5);
s1.setSemesterCGPA(2, 8.8);
s1.setSemesterCGPA(3, 9.0);
s1.setSemesterCGPA(4, 9.2);
s1.display();
cout << "\n=== Copy constructor ===\n";
Student s2 = s1; // deep copy
s2.display();
cout << "\n=== Move constructor ===\n";
Student s3 = move(s1); // steal s1's resources
s3.display();
cout << "\nTotal students alive: " << Student::getTotalStudents() << "\n";
cout << "\n=== Destructors called as scope ends ===\n";
return 0;
}
Sample Output:
=== Default constructor ===
[Default] Student created: Unknown
=== Parameterised constructor ===
[Param] Student created: Aditya Kumar
Name: Aditya Kumar, Roll: 101, Avg CGPA: 8.875
=== Copy constructor ===
[Copy] Copied student: Aditya Kumar
Name: Aditya Kumar, Roll: 101, Avg CGPA: 8.875
=== Move constructor ===
[Move] Moved student: Aditya Kumar
Name: Aditya Kumar, Roll: 101, Avg CGPA: 8.875
Total students alive: 4
=== Destructors called as scope ends ===
[Destruct] Student destroyed: Aditya Kumar
[Destruct] Student destroyed: Aditya Kumar
[Destruct] Student destroyed:
[Destruct] Student destroyed: Unknown
Common Pitfalls
- Forgetting a destructor when you use
new: Everynewneeds adelete. If your class owns heap memory and you forget todelete[]in the destructor, you have a memory leak. - Shallow copy of raw pointers: The compiler-generated copy constructor does a shallow copy. Two objects then point to the same heap memory; when both are destroyed, you get a double-free crash.
- Member initialisation order vs initialiser list order: Members are initialised in declaration order, not initialiser-list order. If
bdepends onabutbis declared first, you get a bug. - Not marking move constructors
noexcept: STL containers (likestd::vector) will fall back to copying instead of moving if the move constructor is notnoexcept. - Calling virtual functions from constructors/destructors: The virtual dispatch mechanism is not fully set up during construction and destruction. Avoid this pattern.
- Using a moved-from object: After
move(s1),s1is in a valid but unspecified state. Do not read from it unless you reset it first.
Practice Exercises
- Write a
DynamicStringclass that manages achar*on the heap. Implement the default constructor, parameterised constructor, copy constructor, and destructor. Verify that copying works correctly by modifying one copy and checking the other is unchanged. - Add a copy assignment operator (
operator=) to theStudentclass above, following the copy-and-swap idiom. - Create a
Timerclass that records the current time in its constructor and prints the elapsed time in its destructor — demonstrating RAII for performance measurement. - Implement a
Stackclass backed by a heap-allocated array with a parameterised constructor, copy constructor, move constructor, and destructor. - Modify the
Studentclass to usestd::vector<double>instead of a rawdouble*array. Notice how the need for a custom copy constructor, move constructor, and destructor disappears — the vector handles them automatically.
Summary
- A constructor initialises an object at the moment it is created; it has the class name and no return type.
- The default constructor takes no arguments; the compiler generates one unless you define any constructor.
- A parameterised constructor accepts arguments, letting each object start with different state.
- The initialiser list (after
:) initialises members more efficiently than assigning in the body; it is mandatory forconstmembers and references. - The copy constructor
(const T& other)creates a new object as a clone; write a deep copy when the class owns raw pointers. - The move constructor
(T&& other) noexceptsteals resources from a temporary, making it far cheaper than copying. - Delegating constructors let one constructor call another to eliminate duplicated initialisation logic.
- The destructor (
~ClassName()) runs automatically when an object is destroyed; release all acquired resources here. - RAII ties resource lifetime to object lifetime: acquire in the constructor, release in the destructor — the foundation of exception-safe C++.