Chapter 9 of 23

Constructors and Destructors

Explore default, parameterised, copy, and move constructors, initialiser lists, delegating constructors, destructors, and RAII — with a full Student class worked example.

Meritshot11 min read
C++ConstructorsDestructorsRAIIMove SemanticsCopy Constructor
All C++ Chapters

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: Every new needs a delete. If your class owns heap memory and you forget to delete[] 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 b depends on a but b is declared first, you get a bug.
  • Not marking move constructors noexcept: STL containers (like std::vector) will fall back to copying instead of moving if the move constructor is not noexcept.
  • 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), s1 is in a valid but unspecified state. Do not read from it unless you reset it first.

Practice Exercises

  1. Write a DynamicString class that manages a char* 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.
  2. Add a copy assignment operator (operator=) to the Student class above, following the copy-and-swap idiom.
  3. Create a Timer class that records the current time in its constructor and prints the elapsed time in its destructor — demonstrating RAII for performance measurement.
  4. Implement a Stack class backed by a heap-allocated array with a parameterised constructor, copy constructor, move constructor, and destructor.
  5. Modify the Student class to use std::vector<double> instead of a raw double* 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 for const members 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) noexcept steals 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++.