Object-Oriented Programming

Constructors and Destructors

Master C++ constructors and destructors — default, parameterised, copy, and move constructors, initialiser lists, the Rule of Five, and RAII.

Constructors and Destructors

A constructor is called automatically when an object is created. A destructor is called automatically when the object goes out of scope or is deleted. Together, they enforce the RAII (Resource Acquisition Is Initialisation) pattern — the single most important C++ idiom for safe resource management.


Default Constructor

cpp
#include <iostream>
#include <string>
using namespace std;

class Point {
public:
    double x, y;

    // Default constructor — called when no arguments are given
    Point() : x(0.0), y(0.0) {
        cout << "Point default-constructed at (0, 0)\n";
    }
};

int main() {
    Point p;          // Calls default constructor
    cout << p.x << ", " << p.y << "\n";  // 0, 0

    Point arr[3];     // Each element default-constructed

    return 0;
}

If you define no constructor at all, the compiler generates a default one for you. As soon as you define any constructor, the compiler's default is suppressed unless you explicitly request it with = default.


Parameterised Constructor

cpp
#include <iostream>
#include <string>
using namespace std;

class Person {
    string name;
    int    age;

public:
    // Parameterised constructor
    Person(const string& name, int age) : name(name), age(age) {
        cout << "Person created: " << this->name << "\n";
    }

    void greet() const {
        cout << "Hi, I'm " << name << " (" << age << ")\n";
    }
};

int main() {
    Person alice("Alice", 30);   // Direct initialisation
    Person bob = Person("Bob", 25);  // Also OK

    alice.greet();   // Hi, I'm Alice (30)
    bob.greet();     // Hi, I'm Bob (25)

    return 0;
}

Member Initialiser List

Always prefer the member initialiser list (after the : before {) over assignments in the constructor body:

cpp
#include <iostream>
#include <string>
using namespace std;

class Circle {
    double radius;
    string colour;
    double area;       // Derived — computed in init list

public:
    // Initialiser list — members are constructed IN DECLARATION ORDER
    Circle(double r, const string& c)
        : radius(r),
          colour(c),
          area(3.14159265358979 * r * r)   // Can use previously listed members
    {
        // Constructor body — for complex logic, input validation
        if (radius < 0) throw invalid_argument("Radius cannot be negative");
    }

    void print() const {
        cout << colour << " circle r=" << radius << " area=" << area << "\n";
    }
};

int main() {
    Circle c(5.0, "red");
    c.print();   // red circle r=5 area=78.5398

    return 0;
}

Note: Members are initialised in the order they are declared in the class, not the order listed in the initialiser list. Always list them in declaration order to avoid subtle bugs.


Destructor

cpp
#include <iostream>
#include <string>
using namespace std;

class FileHandle {
    string filename;
    bool   isOpen;

public:
    FileHandle(const string& name) : filename(name), isOpen(true) {
        cout << "Opening " << filename << "\n";
        // In real code: open the file here
    }

    ~FileHandle() {             // Destructor — same name with ~
        if (isOpen) {
            cout << "Closing " << filename << "\n";
            // In real code: close the file here
            isOpen = false;
        }
    }

    void write(const string& data) {
        if (isOpen) cout << "Writing to " << filename << ": " << data << "\n";
    }
};

int main() {
    {
        FileHandle f("data.txt");   // Constructor called
        f.write("Hello");
    }   // Destructor called automatically here (scope ends)

    cout << "Back in main — file was closed\n";
    return 0;
}
// Output:
// Opening data.txt
// Writing to data.txt: Hello
// Closing data.txt
// Back in main — file was closed

The destructor fires automatically when the object leaves scope — this is RAII in action. You never need to manually call ~FileHandle().


Copy Constructor

cpp
#include <iostream>
#include <string>
using namespace std;

class Matrix {
    int    rows, cols;
    int*   data;    // Raw pointer — requires custom copy

public:
    Matrix(int r, int c) : rows(r), cols(c), data(new int[r * c]()) {
        cout << "Matrix(" << r << "x" << c << ") constructed\n";
    }

    // Copy constructor — DEEP COPY (copy the data, not just the pointer)
    Matrix(const Matrix& other) : rows(other.rows), cols(other.cols),
                                   data(new int[other.rows * other.cols]) {
        cout << "Matrix copied\n";
        for (int i = 0; i < rows * cols; i++) {
            data[i] = other.data[i];
        }
    }

    ~Matrix() {
        delete[] data;
        cout << "Matrix destroyed\n";
    }

    void set(int r, int c, int val) { data[r * cols + c] = val; }
    int  get(int r, int c) const    { return data[r * cols + c]; }
};

int main() {
    Matrix m1(2, 2);
    m1.set(0, 0, 42);

    Matrix m2 = m1;            // Copy constructor called
    cout << m2.get(0, 0) << "\n";  // 42 — deep copy

    m1.set(0, 0, 99);
    cout << m2.get(0, 0) << "\n";  // 42 — m2 is independent!

    return 0;
}
// Matrix(2x2) constructed
// Matrix copied
// 42
// 42
// Matrix destroyed
// Matrix destroyed

Without a custom copy constructor, the compiler generates a shallow copy — both objects share the same pointer, leading to a double-free crash.


The Rule of Three / Five / Zero

Whenever your class manually manages a resource (memory, file, socket):

RuleWhat to define
Rule of Three (C++03)Destructor + Copy Constructor + Copy Assignment Operator
Rule of Five (C++11)+ Move Constructor + Move Assignment Operator
Rule of Zero (best)Let smart pointers manage resources — define no special functions
cpp
class Resource {
    int* data;
public:
    // 1. Constructor
    Resource(int n) : data(new int[n]) {}

    // 2. Destructor
    ~Resource() { delete[] data; }

    // 3. Copy constructor
    Resource(const Resource& other) : data(new int[1]) {
        data[0] = other.data[0];
    }

    // 4. Copy assignment
    Resource& operator=(const Resource& other) {
        if (this != &other) {          // Protect against self-assignment
            delete[] data;
            data = new int[1];
            data[0] = other.data[0];
        }
        return *this;
    }

    // 5. Move constructor (C++11)
    Resource(Resource&& other) noexcept : data(other.data) {
        other.data = nullptr;          // Leave other in valid empty state
    }

    // 6. Move assignment (C++11)
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
};

Delegating Constructors (C++11)

One constructor can call another to avoid code duplication:

cpp
class Config {
    string host;
    int    port;
    bool   useSSL;

public:
    // Main constructor
    Config(const string& host, int port, bool ssl)
        : host(host), port(port), useSSL(ssl) {}

    // Delegates to main — adds defaults
    Config(const string& host) : Config(host, 443, true) {}

    // Delegates to main — default everything
    Config() : Config("localhost", 8080, false) {}

    void print() const {
        cout << (useSSL ? "https" : "http") << "://"
             << host << ":" << port << "\n";
    }
};

int main() {
    Config c1;
    Config c2("api.example.com");
    Config c3("db.local", 5432, false);

    c1.print();  // http://localhost:8080
    c2.print();  // https://api.example.com:443
    c3.print();  // http://db.local:5432

    return 0;
}

Practical Example: RAII File Wrapper

cpp
#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
using namespace std;

class SafeFile {
    fstream   stream;
    string    path;
    bool      writable;

public:
    // Constructor — opens the file
    SafeFile(const string& filePath, bool write = false)
        : path(filePath), writable(write) {
        ios::openmode mode = write
            ? (ios::out | ios::trunc)
            : ios::in;
        stream.open(filePath, mode);
        if (!stream.is_open()) {
            throw runtime_error("Cannot open: " + filePath);
        }
        cout << "Opened: " << path << "\n";
    }

    // Destructor — closes the file (RAII)
    ~SafeFile() {
        if (stream.is_open()) {
            stream.close();
            cout << "Closed: " << path << "\n";
        }
    }

    // Prevent copying (file handles should not be copied)
    SafeFile(const SafeFile&) = delete;
    SafeFile& operator=(const SafeFile&) = delete;

    void writeLine(const string& line) {
        if (!writable) throw logic_error("File not opened for writing");
        stream << line << "\n";
    }

    bool readLine(string& line) {
        return static_cast<bool>(getline(stream, line));
    }
};

int main() {
    try {
        // Write phase
        {
            SafeFile writer("output.txt", true);
            writer.writeLine("Hello from RAII!");
            writer.writeLine("This file closes automatically.");
        }  // SafeFile destructor runs here

        // Read phase
        SafeFile reader("output.txt");
        string line;
        while (reader.readLine(line)) {
            cout << "> " << line << "\n";
        }
    } catch (const exception& e) {
        cerr << "Error: " << e.what() << "\n";
    }
    return 0;
}

Summary

  • Constructor runs automatically when an object is created — use the member initialiser list to initialise members before the body runs
  • Destructor runs automatically when an object leaves scope — the RAII pattern guarantees cleanup without try/finally
  • Provide a copy constructor and copy assignment whenever your class owns a raw pointer — otherwise the default shallow copy causes double-free bugs
  • The Rule of Five: if you define any of destructor, copy constructor, copy assignment, move constructor, or move assignment — define all five
  • The Rule of Zero: design classes to use smart pointers or standard containers, so you need no custom special functions
  • Delegating constructors let one constructor call another — eliminates duplication in constructor overloads
  • Use = delete to explicitly forbid copying (sockets, file handles, mutexes)

Next, we'll cover inheritance — building new classes from existing ones using C++ class hierarchies.