Menu

System Programming Topic

C++ Advanced OOP & Operator Overloading

1. Operator Overloading

Operator overloading allows you to redefine the behavior of operators for user-defined types.

Why Overload Operators?

  • Make classes behave like built-in types

  • Improve code readability

  • Enable intuitive syntax

  • Maintain consistency

Basic Syntax:

cpp

ReturnType operatorOp(parameters) {
    // implementation
}

Overloading Unary Operators

Unary operators work on a single operand.

cpp

class Counter {
private:
    int count;
public:
    Counter(int c = 0) : count(c) {}
    
    // Overload prefix increment (++obj)
    Counter& operator++() {
        ++count;
        return *this;  // Return reference to allow chaining
    }
    
    // Overload postfix increment (obj++)
    Counter operator++(int) {  // int dummy parameter distinguishes postfix
        Counter temp = *this;  // Create copy of current state
        ++(*this);            // Increment current object
        return temp;          // Return old value (by value, not reference)
    }
    
    // Overload prefix decrement (--obj)
    Counter& operator--() {
        --count;
        return *this;
    }
    
    // Overload postfix decrement (obj--)
    Counter operator--(int) {
        Counter temp = *this;
        --(*this);
        return temp;
    }
    
    // Overload unary minus (-obj)
    Counter operator-() const {
        return Counter(-count);
    }
    
    // Overload logical NOT (!obj)
    bool operator!() const {
        return count == 0;
    }
    
    // Overload address-of (&obj)
    Counter* operator&() {
        return this;
    }
    
    // Overload dereference (*obj) - for pointer-like classes
    int operator*() const {
        return count;
    }
    
    void display() const {
        cout << "Count: " << count << endl;
    }
};

// Usage
int main() {
    Counter c1(5);
    
    // Prefix increment
    ++c1;           // c1 becomes 6
    Counter c2 = ++c1;  // c1 becomes 7, c2 is 7
    
    // Postfix increment
    Counter c3 = c1++;  // c3 is 7, c1 becomes 8
    
    // Unary minus
    Counter c4 = -c1;   // c4 is -8, c1 remains 8
    
    // Logical NOT
    Counter c5(0);
    if(!c5) {
        cout << "Counter is zero" << endl;
    }
    
    // Chaining
    Counter c6(10);
    ++++c6;  // Valid: becomes 12
    // c6++++;  // Invalid: cannot chain postfix
    
    return 0;
}

Overloading Binary Operators

Binary operators work with two operands.

cpp

class Complex {
private:
    double real, imag;
public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
    
    // Overload addition (obj1 + obj2)
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }
    
    // Overload subtraction (obj1 - obj2)
    Complex operator-(const Complex& other) const {
        return Complex(real - other.real, imag - other.imag);
    }
    
    // Overload multiplication (obj1 * obj2)
    Complex operator*(const Complex& other) const {
        return Complex(
            real * other.real - imag * other.imag,
            real * other.imag + imag * other.real
        );
    }
    
    // Overload division (obj1 / obj2)
    Complex operator/(const Complex& other) const {
        double denominator = other.real * other.real + other.imag * other.imag;
        return Complex(
            (real * other.real + imag * other.imag) / denominator,
            (imag * other.real - real * other.imag) / denominator
        );
    }
    
    // Overload equality (obj1 == obj2)
    bool operator==(const Complex& other) const {
        return real == other.real && imag == other.imag;
    }
    
    // Overload inequality (obj1 != obj2)
    bool operator!=(const Complex& other) const {
        return !(*this == other);
    }
    
    // Overload less than (obj1 < obj2) - for sorting
    bool operator<(const Complex& other) const {
        // Compare magnitude
        double mag1 = real * real + imag * imag;
        double mag2 = other.real * other.real + other.imag * other.imag;
        return mag1 < mag2;
    }
    
    // Overload compound assignment (+=)
    Complex& operator+=(const Complex& other) {
        real += other.real;
        imag += other.imag;
        return *this;
    }
    
    // Overload compound assignment (-=)
    Complex& operator-=(const Complex& other) {
        real -= other.real;
        imag -= other.imag;
        return *this;
    }
    
    // Overload subscript [] - useful for array-like classes
    double operator[](int index) const {
        if(index == 0) return real;
        if(index == 1) return imag;
        throw out_of_range("Invalid index");
    }
    
    // Overload function call () - functors
    Complex operator()(double scale) const {
        return Complex(real * scale, imag * scale);
    }
    
    void display() const {
        cout << real << (imag >= 0 ? " + " : " - ") << abs(imag) << "i" << endl;
    }
};

// Usage
int main() {
    Complex c1(3.0, 4.0);
    Complex c2(1.0, 2.0);
    
    Complex c3 = c1 + c2;  // 4.0 + 6.0i
    Complex c4 = c1 - c2;  // 2.0 + 2.0i
    Complex c5 = c1 * c2;  // -5.0 + 10.0i
    Complex c6 = c1 / c2;  // 2.2 - 0.4i
    
    if(c1 == c2) {
        cout << "Equal" << endl;
    }
    
    if(c1 != c2) {
        cout << "Not equal" << endl;
    }
    
    c1 += c2;  // c1 becomes 4.0 + 6.0i
    
    double realPart = c1[0];  // 4.0
    double imagPart = c1[1];  // 6.0
    
    Complex scaled = c1(2.0);  // 8.0 + 12.0i
    
    return 0;
}

Overloading << and >> (Stream Insertion/Extraction)

These must be overloaded as non-member functions.

cpp

#include <iostream>
using namespace std;

class Date {
private:
    int day, month, year;
public:
    Date(int d = 1, int m = 1, int y = 2000) : day(d), month(m), year(y) {}
    
    // Getter methods for friend function access
    int getDay() const { return day; }
    int getMonth() const { return month; }
    int getYear() const { return year; }
    
    // Setter methods
    void setDate(int d, int m, int y) {
        day = d; month = m; year = y;
    }
    
    // Friend declaration for stream operators
    friend ostream& operator<<(ostream& os, const Date& dt);
    friend istream& operator>>(istream& is, Date& dt);
};

// Overload << for output (must be non-member)
ostream& operator<<(ostream& os, const Date& dt) {
    os << dt.day << '/' << dt.month << '/' << dt.year;
    return os;  // Must return stream for chaining
}

// Overload >> for input (must be non-member)
istream& operator>>(istream& is, Date& dt) {
    char slash1, slash2;
    is >> dt.day >> slash1 >> dt.month >> slash2 >> dt.year;
    
    // Validate input
    if(slash1 != '/' || slash2 != '/') {
        is.setstate(ios::failbit);  // Set fail bit
    }
    
    return is;
}

// Alternative approach using getters/setters (no friend needed)
ostream& operator<<(ostream& os, const Date& dt) {
    os << dt.getDay() << '/' << dt.getMonth() << '/' << dt.getYear();
    return os;
}

istream& operator>>(istream& is, Date& dt) {
    int d, m, y;
    char slash1, slash2;
    
    if(is >> d >> slash1 >> m >> slash2 >> y) {
        if(slash1 == '/' && slash2 == '/') {
            dt.setDate(d, m, y);
        } else {
            is.setstate(ios::failbit);
        }
    }
    
    return is;
}

// Usage
int main() {
    Date today(25, 12, 2024);
    Date birthday;
    
    // Output using overloaded <<
    cout << "Today is: " << today << endl;  // 25/12/2024
    
    // Input using overloaded >>
    cout << "Enter your birthday (dd/mm/yyyy): ";
    cin >> birthday;
    
    if(cin) {  // Check if input succeeded
        cout << "Your birthday: " << birthday << endl;
    } else {
        cout << "Invalid date format!" << endl;
        cin.clear();  // Clear error state
    }
    
    // Chaining
    cout << "Date1: " << today << ", Date2: " << birthday << endl;
    
    return 0;
}

Overloading Assignment Operator (=)

cpp

class String {
private:
    char* str;
    int length;
    
public:
    // Constructor
    String(const char* s = "") {
        length = strlen(s);
        str = new char[length + 1];
        strcpy(str, s);
    }
    
    // Destructor
    ~String() {
        delete[] str;
    }
    
    // Copy constructor
    String(const String& other) {
        length = other.length;
        str = new char[length + 1];
        strcpy(str, other.str);
    }
    
    // Overload assignment operator
    String& operator=(const String& other) {
        // Check for self-assignment
        if(this != &other) {
            // Delete old memory
            delete[] str;
            
            // Allocate new memory
            length = other.length;
            str = new char[length + 1];
            strcpy(str, other.str);
        }
        return *this;
    }
    
    // Move assignment (C++11)
    String& operator=(String&& other) noexcept {
        if(this != &other) {
            delete[] str;
            str = other.str;
            length = other.length;
            
            // Leave other in valid state
            other.str = nullptr;
            other.length = 0;
        }
        return *this;
    }
    
    // Overload += operator
    String& operator+=(const String& other) {
        char* temp = new char[length + other.length + 1];
        strcpy(temp, str);
        strcat(temp, other.str);
        
        delete[] str;
        str = temp;
        length += other.length;
        
        return *this;
    }
    
    // Overload + operator (non-member recommended)
    friend String operator+(const String& lhs, const String& rhs);
    
    void display() const {
        cout << str << endl;
    }
};

// Overload + as non-member function
String operator+(const String& lhs, const String& rhs) {
    String result = lhs;  // Uses copy constructor
    result += rhs;        // Uses += operator
    return result;
}

// Usage
int main() {
    String s1 = "Hello";
    String s2 = " World";
    
    String s3 = s1 + s2;  // Uses overloaded +
    s3.display();         // "Hello World"
    
    s1 = s2;  // Uses overloaded =
    s1.display();  // " World"
    
    // Self-assignment check
    s1 = s1;  // Safe due to self-assignment check
    
    return 0;
}

Best Practices for Operator Overloading

cpp

// 1. FOLLOW OPERATOR SEMANTICS
//    + should add, * should multiply, etc.
//    Don't make + perform subtraction!

class Vector {
    double x, y;
public:
    // Good: Follows mathematical convention
    Vector operator+(const Vector& v) const {
        return Vector(x + v.x, y + v.y);
    }
    
    // Bad: Confusing
    Vector operator+(const Vector& v) const {
        return Vector(x - v.x, y - v.y);  // WRONG!
    }
};

// 2. RETURN APPROPRIATE TYPES
//    - Compound assignments should return reference
//    - Arithmetic should return new object
//    - Comparisons should return bool

class Money {
    int dollars, cents;
public:
    // Good: Compound assignment returns reference
    Money& operator+=(const Money& other) {
        // implementation
        return *this;
    }
    
    // Good: Arithmetic returns new object
    Money operator+(const Money& other) const {
        Money result = *this;
        result += other;
        return result;
    }
    
    // Good: Comparison returns bool
    bool operator==(const Money& other) const {
        return dollars == other.dollars && cents == other.cents;
    }
};

// 3. MAINTAIN SYMMETRY
//    For commutative operators, support different operand types

class Number {
    int value;
public:
    Number(int v = 0) : value(v) {}
    
    // Overload for Number + int
    Number operator+(int n) const {
        return Number(value + n);
    }
    
    // Overload for int + Number (must be non-member)
    friend Number operator+(int n, const Number& num);
};

// Non-member function for symmetry
Number operator+(int n, const Number& num) {
    return num + n;  // Reuse existing operator
}

// 4. USE NON-MEMBER FUNCTIONS WHEN POSSIBLE
//    Especially for symmetric operators

class Fraction {
    int num, den;
public:
    Fraction(int n = 0, int d = 1) : num(n), den(d) {}
    
    // Member function for compound assignment
    Fraction& operator*=(const Fraction& f) {
        num *= f.num;
        den *= f.den;
        return *this;
    }
    
    // Getter functions
    int numerator() const { return num; }
    int denominator() const { return den; }
};

// Non-member for symmetry
Fraction operator*(const Fraction& f1, const Fraction& f2) {
    Fraction result = f1;
    result *= f2;
    return result;
}

// 5. PROVIDE CONSISTENT OPERATOR SETS
//    If you provide ==, also provide !=
//    If you provide +, consider +=
//    If you provide <, consider >, <=, >=

class Point {
    int x, y;
public:
    Point(int x = 0, int y = 0) : x(x), y(y) {}
    
    // Provide full set of comparison operators
    bool operator==(const Point& p) const { return x == p.x && y == p.y; }
    bool operator!=(const Point& p) const { return !(*this == p); }
    bool operator<(const Point& p) const { 
        return x < p.x || (x == p.x && y < p.y); 
    }
    bool operator>(const Point& p) const { return p < *this; }
    bool operator<=(const Point& p) const { return !(p < *this); }
    bool operator>=(const Point& p) const { return !(*this < p); }
};

// 6. HANDLE SELF-ASSIGNMENT IN = OPERATOR
// 7. MAKE OPERATORS CONST-CORRECT
// 8. PREFER PASS BY CONST REFERENCE FOR PARAMETERS
// 9. CONSIDER PERFORMANCE (use move semantics in C++11+)

2. Friend Functions & Classes

Friend Function

A friend function is a non-member function that has access to private and protected members of a class.

cpp

class Box {
private:
    double length, width, height;
    
public:
    Box(double l = 0.0, double w = 0.0, double h = 0.0) 
        : length(l), width(w), height(h) {}
    
    // Calculate volume (member function)
    double volume() const {
        return length * width * height;
    }
    
    // Friend function declaration
    friend void printDimensions(const Box& box);
    friend double calculateSurfaceArea(const Box& box);
    friend Box operator+(const Box& b1, const Box& b2);
};

// Friend function definition
void printDimensions(const Box& box) {
    // Can access private members
    cout << "Length: " << box.length 
         << ", Width: " << box.width 
         << ", Height: " << box.height << endl;
}

double calculateSurfaceArea(const Box& box) {
    return 2 * (box.length * box.width + 
                box.length * box.height + 
                box.width * box.height);
}

// Friend operator overload
Box operator+(const Box& b1, const Box& b2) {
    return Box(b1.length + b2.length,
               b1.width + b2.width,
               b1.height + b2.height);
}

// Usage
int main() {
    Box box1(2.0, 3.0, 4.0);
    Box box2(1.0, 1.0, 1.0);
    
    printDimensions(box1);  // Can access private members
    cout << "Surface area: " << calculateSurfaceArea(box1) << endl;
    
    Box box3 = box1 + box2;  // Using friend operator
    printDimensions(box3);
    
    return 0;
}

Friend Class

A friend class is a class whose all member functions are friend functions of another class.

cpp

class Storage;  // Forward declaration

class Display {
private:
    bool displayInHex;
    
public:
    Display(bool hex = false) : displayInHex(hex) {}
    
    // Member function that accesses Storage's private members
    void displayItem(const Storage& storage);
};

class Storage {
private:
    int nValue;
    double dValue;
    
public:
    Storage(int n, double d) : nValue(n), dValue(d) {}
    
    // Make Display a friend of Storage
    friend class Display;
    
    // Make specific function friend
    friend void Display::displayItem(const Storage& storage);
};

// Display member function definition
void Display::displayItem(const Storage& storage) {
    if(displayInHex) {
        cout << hex << storage.nValue << endl;
    } else {
        cout << storage.nValue << endl;
    }
    cout << storage.dValue << endl;
}

// More complex example
class Matrix;

class Vector {
private:
    int size;
    double* data;
    
public:
    Vector(int s = 0) : size(s), data(new double[s]) {}
    ~Vector() { delete[] data; }
    
    // Friend declaration for Matrix
    friend class Matrix;
    
    // Friend function for dot product
    friend double dotProduct(const Vector& v1, const Vector& v2);
    
    double& operator[](int index) { return data[index]; }
    const double& operator[](int index) const { return data[index]; }
};

class Matrix {
private:
    int rows, cols;
    double** data;
    
public:
    Matrix(int r, int c) : rows(r), cols(c) {
        data = new double*[rows];
        for(int i = 0; i < rows; i++) {
            data[i] = new double[cols];
        }
    }
    
    ~Matrix() {
        for(int i = 0; i < rows; i++) {
            delete[] data[i];
        }
        delete[] data;
    }
    
    // Can access Vector's private members
    Vector multiply(const Vector& v) const {
        if(cols != v.size) {
            throw invalid_argument("Dimension mismatch");
        }
        
        Vector result(rows);
        for(int i = 0; i < rows; i++) {
            result[i] = 0;
            for(int j = 0; j < cols; j++) {
                result[i] += data[i][j] * v.data[j];  // Access private member
            }
        }
        return result;
    }
};

double dotProduct(const Vector& v1, const Vector& v2) {
    if(v1.size != v2.size) {
        throw invalid_argument("Vectors must have same size");
    }
    
    double result = 0;
    for(int i = 0; i < v1.size; i++) {
        result += v1.data[i] * v2.data[i];  // Access private members
    }
    return result;
}

Use Cases for Friend Functions/Classes

cpp

// 1. OPERATOR OVERLOADING WHEN SYMMETRY IS NEEDED
class Complex {
    double real, imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // Need friend for symmetric multiplication with double
    friend Complex operator*(const Complex& c, double d);
    friend Complex operator*(double d, const Complex& c);
    
    double getReal() const { return real; }
    double getImag() const { return imag; }
};

Complex operator*(const Complex& c, double d) {
    return Complex(c.real * d, c.imag * d);
}

Complex operator*(double d, const Complex& c) {
    return c * d;  // Reuse above function
}

// 2. IMPLEMENTING ITERATORS OR RELATED CLASSES
class Container {
private:
    int* data;
    int size;
    
public:
    class Iterator {
    private:
        Container* container;
        int index;
        
    public:
        Iterator(Container* c, int i) : container(c), index(i) {}
        
        // Iterator needs access to Container's private data
        friend class Container;
        
        int& operator*() {
            return container->data[index];
        }
        
        Iterator& operator++() {
            index++;
            return *this;
        }
        
        bool operator!=(const Iterator& other) {
            return index != other.index;
        }
    };
    
    Container(int s) : size(s), data(new int[s]) {}
    ~Container() { delete[] data; }
    
    Iterator begin() { return Iterator(this, 0); }
    Iterator end() { return Iterator(this, size); }
    
    // Friend declaration for Iterator
    friend class Iterator;
};

// 3. IMPLEMENTING BIDIRECTIONAL ASSOCIATIONS
class Department;  // Forward declaration

class Employee {
private:
    string name;
    Department* dept;  // Pointer to associated department
    
public:
    Employee(string n) : name(n), dept(nullptr) {}
    
    // Friend declaration
    friend class Department;
    
    string getName() const { return name; }
};

class Department {
private:
    string name;
    vector<Employee*> employees;
    
public:
    Department(string n) : name(n) {}
    
    void addEmployee(Employee* emp) {
        employees.push_back(emp);
        emp->dept = this;  // Can access Employee's private member
    }
    
    void displayEmployees() {
        cout << "Department: " << name << endl;
        for(auto emp : employees) {
            cout << "  - " << emp->getName() << endl;
        }
    }
    
    // Friend declaration
    friend class Employee;
};

// 4. UNIT TESTING (Access private members for testing)
class BankAccount {
private:
    double balance;
    string accountNumber;
    
public:
    BankAccount(string accNum, double initial = 0) 
        : accountNumber(accNum), balance(initial) {}
    
    void deposit(double amount) { balance += amount; }
    bool withdraw(double amount) {
        if(amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }
    
    double getBalance() const { return balance; }
    
    // Friend class for testing
    friend class BankAccountTest;
};

class BankAccountTest {
public:
    static bool testDeposit(BankAccount& acc) {
        double oldBalance = acc.balance;  // Access private member
        acc.deposit(100);
        return acc.balance == oldBalance + 100;
    }
    
    static bool testWithdraw(BankAccount& acc) {
        acc.balance = 200;  // Directly set private member for test
        bool result = acc.withdraw(100);
        return result && acc.balance == 100;
    }
};

When to Use Friend Functions/Classes

cpp

// ✅ GOOD USE CASES:

// 1. Operator overloading for symmetric operators
// 2. Implementing related classes (iterators, builders, etc.)
// 3. Utility functions that need direct access
// 4. Unit testing classes

// ❌ BAD USE CASES / ALTERNATIVES:

// 1. Overusing friendship - breaks encapsulation
//    Consider: Use public getter/setter methods instead
class BadExample {
private:
    int secret;
    
public:
    // Instead of making everything friend, provide accessors
    int getSecret() const { return secret; }
    void setSecret(int s) { secret = s; }
};

// 2. Making unrelated classes friends
//    Consider: Refactor or use inheritance

// 3. Using friendship to avoid proper design
//    Consider: Re-evaluate class relationships

3. Virtual Destructors

Why Virtual Destructors Matter

cpp

#include <iostream>
using namespace std;

// Base class WITHOUT virtual destructor
class BaseNoVirtual {
protected:
    int* data;
    
public:
    BaseNoVirtual() {
        data = new int[100];
        cout << "BaseNoVirtual: Memory allocated" << endl;
    }
    
    ~BaseNoVirtual() {  // NON-virtual destructor
        delete[] data;
        cout << "BaseNoVirtual: Memory freed" << endl;
    }
    
    virtual void show() {
        cout << "BaseNoVirtual::show()" << endl;
    }
};

// Derived class
class DerivedNoVirtual : public BaseNoVirtual {
private:
    int* moreData;
    
public:
    DerivedNoVirtual() : BaseNoVirtual() {
        moreData = new int[50];
        cout << "DerivedNoVirtual: More memory allocated" << endl;
    }
    
    ~DerivedNoVirtual() {
        delete[] moreData;
        cout << "DerivedNoVirtual: More memory freed" << endl;
    }
    
    void show() override {
        cout << "DerivedNoVirtual::show()" << endl;
    }
};

// Base class WITH virtual destructor
class BaseVirtual {
protected:
    int* data;
    
public:
    BaseVirtual() {
        data = new int[100];
        cout << "BaseVirtual: Memory allocated" << endl;
    }
    
    virtual ~BaseVirtual() {  // VIRTUAL destructor
        delete[] data;
        cout << "BaseVirtual: Memory freed" << endl;
    }
    
    virtual void show() {
        cout << "BaseVirtual::show()" << endl;
    }
};

// Derived class
class DerivedVirtual : public BaseVirtual {
private:
    int* moreData;
    
public:
    DerivedVirtual() : BaseVirtual() {
        moreData = new int[50];
        cout << "DerivedVirtual: More memory allocated" << endl;
    }
    
    ~DerivedVirtual() override {  // Override virtual destructor
        delete[] moreData;
        cout << "DerivedVirtual: More memory freed" << endl;
    }
    
    void show() override {
        cout << "DerivedVirtual::show()" << endl;
    }
};

int main() {
    cout << "=== WITHOUT Virtual Destructor ===" << endl;
    {
        BaseNoVirtual* ptr = new DerivedNoVirtual();
        ptr->show();
        delete ptr;  // PROBLEM: Only Base destructor called!
        // Output:
        // BaseNoVirtual: Memory allocated
        // DerivedNoVirtual: More memory allocated
        // DerivedNoVirtual::show()
        // BaseNoVirtual: Memory freed
        // MEMORY LEAK: moreData not freed!
    }
    
    cout << "\n=== WITH Virtual Destructor ===" << endl;
    {
        BaseVirtual* ptr = new DerivedVirtual();
        ptr->show();
        delete ptr;  // Correct: Both destructors called
        // Output:
        // BaseVirtual: Memory allocated
        // DerivedVirtual: More memory allocated
        // DerivedVirtual::show()
        // DerivedVirtual: More memory freed
        // BaseVirtual: Memory freed
        // NO MEMORY LEAK
    }
    
    return 0;
}

Memory Leak Prevention

cpp

// Complete example demonstrating the importance

class Resource {
private:
    int* resource;
    string name;
    
public:
    Resource(string n) : name(n) {
        resource = new int[100];
        cout << name << ": Resource allocated" << endl;
    }
    
    virtual ~Resource() {
        delete[] resource;
        cout << name << ": Resource freed" << endl;
    }
    
    virtual void use() {
        cout << name << ": Using resource" << endl;
    }
};

class ExtendedResource : public Resource {
private:
    int* extraResource;
    string extraName;
    
public:
    ExtendedResource(string baseName, string extra) 
        : Resource(baseName), extraName(extra) {
        extraResource = new int[50];
        cout << extraName << ": Extra resource allocated" << endl;
    }
    
    ~ExtendedResource() override {
        delete[] extraResource;
        cout << extraName << ": Extra resource freed" << endl;
    }
    
    void use() override {
        Resource::use();
        cout << extraName << ": Using extra resource" << endl;
    }
};

// Factory function
Resource* createResource(bool extended) {
    if(extended) {
        return new ExtendedResource("Base", "Extended");
    } else {
        return new Resource("Simple");
    }
}

// Smart pointer solution (C++11) - even better
#include <memory>

void smartPointerExample() {
    cout << "\n=== Smart Pointer Solution ===" << endl;
    
    // unique_ptr automatically calls correct destructor
    unique_ptr<Resource> ptr = make_unique<ExtendedResource>("SmartBase", "SmartExtended");
    ptr->use();
    // Destructor called automatically when ptr goes out of scope
    
    // shared_ptr also works
    shared_ptr<Resource> sharedPtr = make_shared<ExtendedResource>("SharedBase", "SharedExtended");
    sharedPtr->use();
}

int main() {
    cout << "=== Virtual Destructor Example ===" << endl;
    
    // Using raw pointers (requires virtual destructor)
    Resource* resource = createResource(true);
    resource->use();
    delete resource;  // Correctly calls both destructors if virtual
    
    // What happens without virtual destructor:
    // delete resource would only call ~Resource()
    // ~ExtendedResource() would NOT be called
    // extraResource memory leak!
    
    smartPointerExample();
    
    return 0;
}

Rules and Guidelines

cpp

// RULE 1: If a class has ANY virtual function, it should have a virtual destructor
class Shape {
public:
    virtual void draw() const = 0;  // Pure virtual
    virtual ~Shape() {}              // Virtual destructor (can be empty)
};

// RULE 2: Make base class destructor virtual when using polymorphism
class Animal {
public:
    virtual void speak() const {
        cout << "Animal sound" << endl;
    }
    
    virtual ~Animal() {  // Must be virtual
        cout << "Animal destroyed" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() const override {
        cout << "Woof!" << endl;
    }
    
    ~Dog() override {  // Override is optional but good practice
        cout << "Dog destroyed" << endl;
    }
};

// RULE 3: Abstract classes should have virtual destructors
class AbstractBase {
public:
    virtual void pureVirtual() = 0;  // Makes class abstract
    
    virtual ~AbstractBase() = default;  // Virtual destructor
    // Using = default for trivial destructor
};

// RULE 4: Classes not designed for inheritance should have non-virtual destructors
class FinalClass {
private:
    int data;
    
public:
    FinalClass() : data(0) {}
    ~FinalClass() {  // Non-virtual - don't inherit from this!
        // Cleanup
    }
    
    // Prevent inheritance (C++11)
    // FinalClass() = default;
    // virtual ~FinalClass() = delete;  // Or delete destructor
};

// RULE 5: Use override keyword for destructors in derived classes (C++11)
class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    ~Derived() override {  // Good: clearly shows intent
        // Cleanup
    }
};

// RULE 6: Consider making destructor noexcept (C++11)
class SafeBase {
public:
    virtual ~SafeBase() noexcept {  // Destructors shouldn't throw
        // Cleanup that won't throw
    }
};

Advanced Virtual Destructor Patterns

cpp

// 1. Protected Virtual Destructor for Base Classes
//    Prevents deletion via base pointer, forces use of derived class
class BaseProtected {
protected:
    virtual ~BaseProtected() {  // Protected destructor
        cout << "BaseProtected destroyed" << endl;
    }
    
public:
    virtual void operation() = 0;
    
    // Factory method pattern
    static void destroy(BaseProtected* obj) {
        delete obj;  // Can call protected destructor from static member
    }
};

class DerivedProtected : public BaseProtected {
public:
    ~DerivedProtected() override {
        cout << "DerivedProtected destroyed" << endl;
    }
    
    void operation() override {
        cout << "DerivedProtected operation" << endl;
    }
};

// 2. Virtual Destructor with Smart Pointers
#include <memory>

class Interface {
public:
    virtual void perform() = 0;
    virtual ~Interface() = default;  // Must be virtual
    
protected:
    Interface() = default;
    Interface(const Interface&) = default;
    Interface& operator=(const Interface&) = default;
};

class Implementation : public Interface {
    unique_ptr<int[]> data;
    
public:
    Implementation() : data(new int[100]) {}
    
    void perform() override {
        cout << "Implementation performing" << endl;
    }
    
    ~Implementation() override {
        cout << "Implementation destroyed" << endl;
    }
};

// 3. CRTP (Curiously Recurring Template Pattern) with Virtual Destructor
template<typename Derived>
class BaseCRTP {
public:
    virtual ~BaseCRTP() = default;
    
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
    
    virtual void polymorphic() {
        cout << "BaseCRTP polymorphic" << endl;
    }
};

class DerivedCRTP : public BaseCRTP<DerivedCRTP> {
public:
    void implementation() {
        cout << "DerivedCRTP implementation" << endl;
    }
    
    void polymorphic() override {
        cout << "DerivedCRTP polymorphic" << endl;
    }
    
    ~DerivedCRTP() override {
        cout << "DerivedCRTP destroyed" << endl;
    }
};

// 4. Multiple Inheritance with Virtual Destructors
class Base1 {
public:
    virtual ~Base1() {
        cout << "Base1 destroyed" << endl;
    }
    
    virtual void func1() = 0;
};

class Base2 {
public:
    virtual ~Base2() {
        cout << "Base2 destroyed" << endl;
    }
    
    virtual void func2() = 0;
};

class MultipleDerived : public Base1, public Base2 {
public:
    void func1() override {
        cout << "MultipleDerived func1" << endl;
    }
    
    void func2() override {
        cout << "MultipleDerived func2" << endl;
    }
    
    ~MultipleDerived() override {
        cout << "MultipleDerived destroyed" << endl;
    }
};

void multipleInheritanceExample() {
    MultipleDerived* md = new MultipleDerived();
    Base1* b1 = md;
    Base2* b2 = md;
    
    b1->func1();
    b2->func2();
    
    delete b1;  // Correctly calls all destructors
    // Note: delete b2 would also work, but delete only once!
}

Best Practices Summary

cpp

// 1. ALWAYS declare destructor virtual in polymorphic base classes
class GoodBase {
public:
    virtual ~GoodBase() = default;  // Good
    virtual void doSomething() = 0;
};

// 2. Use override keyword for derived class destructors
class GoodDerived : public GoodBase {
public:
    ~GoodDerived() override {  // Good
        // Cleanup
    }
    
    void doSomething() override {
        // Implementation
    }
};

// 3. Consider making destructor noexcept
class NoExceptBase {
public:
    virtual ~NoExceptBase() noexcept = default;  // Good
};

// 4. Use smart pointers to avoid manual deletion
#include <memory>

void goodPractice() {
    unique_ptr<GoodBase> ptr = make_unique<GoodDerived>();
    // Automatic cleanup with correct destructor
    
    shared_ptr<GoodBase> shared = make_shared<GoodDerived>();
    // Reference counted cleanup
}

// 5. If class shouldn't be inherited from, mark it final (C++11)
class FinalBase final {
public:
    ~FinalBase() {  // Can be non-virtual
        // Cleanup
    }
};

// 6. For abstract interfaces, provide virtual destructor
class Interface {
public:
    virtual void method() = 0;
    virtual ~Interface() = default;  // Required
};

// 7. Document destruction behavior
class Documented {
public:
    /**
     * Virtual destructor ensures proper cleanup
     * of derived class resources when deleted
     * through base pointer.
     */
    virtual ~Documented() = default;
};

// 8. Test destruction behavior
#include <cassert>

void testDestruction() {
    class TestBase {
    public:
        static int destructionCount;
        virtual ~TestBase() { destructionCount++; }
    };
    
    class TestDerived : public TestBase {
    public:
        ~TestDerived() override { destructionCount += 10; }
    };
    
    TestBase::destructionCount = 0;
    TestBase* obj = new TestDerived();
    delete obj;
    
    assert(TestBase::destructionCount == 11);  // Both destructors called
}

Complete Example: Polymorphic Collection

cpp

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

// Base class with virtual destructor
class Media {
protected:
    string title;
    int duration;  // in minutes
    
public:
    Media(const string& t, int d) : title(t), duration(d) {}
    virtual ~Media() {
        cout << "Media destructor: " << title << endl;
    }
    
    virtual void play() const {
        cout << "Playing: " << title << " (" << duration << " mins)" << endl;
    }
    
    virtual void displayInfo() const = 0;  // Pure virtual
    
    // Factory method
    static unique_ptr<Media> createMedia(char type, const string& title, int duration);
};

class Movie : public Media {
private:
    string director;
    
public:
    Movie(const string& t, int d, const string& dir) 
        : Media(t, d), director(dir) {}
    
    ~Movie() override {
        cout << "Movie destructor: " << title << endl;
    }
    
    void play() const override {
        cout << "🎬 Playing movie: " << title 
             << " directed by " << director << endl;
    }
    
    void displayInfo() const override {
        cout << "Movie: " << title << " | Director: " << director 
             << " | Duration: " << duration << " mins" << endl;
    }
};

class Song : public Media {
private:
    string artist;
    string album;
    
public:
    Song(const string& t, int d, const string& art, const string& alb) 
        : Media(t, d), artist(art), album(alb) {}
    
    ~Song() override {
        cout << "Song destructor: " << title << endl;
    }
    
    void play() const override {
        cout << "🎵 Playing song: " << title 
             << " by " << artist << " from album " << album << endl;
    }
    
    void displayInfo() const override {
        cout << "Song: " << title << " | Artist: " << artist 
             << " | Album: " << album << " | Duration: " << duration << " mins" << endl;
    }
};

// Factory implementation
unique_ptr<Media> Media::createMedia(char type, const string& title, int duration) {
    switch(type) {
        case 'M':
            return make_unique<Movie>(title, duration, "Christopher Nolan");
        case 'S':
            return make_unique<Song>(title, duration, "The Beatles", "Abbey Road");
        default:
            return nullptr;
    }
}

// Collection class with operator overloading
class MediaCollection {
private:
    vector<unique_ptr<Media>> collection;
    string name;
    
public:
    MediaCollection(const string& n) : name(n) {}
    
    // Add media (using move semantics)
    void addMedia(unique_ptr<Media> media) {
        collection.push_back(move(media));
    }
    
    // Overload += operator
    MediaCollection& operator+=(unique_ptr<Media> media) {
        collection.push_back(move(media));
        return *this;
    }
    
    // Overload [] operator for access
    Media& operator[](size_t index) {
        if(index >= collection.size()) {
            throw out_of_range("Invalid index");
        }
        return *collection[index];
    }
    
    // Overload << for output
    friend ostream& operator<<(ostream& os, const MediaCollection& mc);
    
    // Play all media
    void playAll() const {
        cout << "\n=== Playing Collection: " << name << " ===" << endl;
        for(const auto& media : collection) {
            media->play();
        }
    }
    
    // Destructor
    ~MediaCollection() {
        cout << "\nDestroying collection: " << name << endl;
        // unique_ptr automatically calls correct destructors
    }
};

// Overload << operator
ostream& operator<<(ostream& os, const MediaCollection& mc) {
    os << "Collection: " << mc.name << " (" << mc.collection.size() << " items)" << endl;
    for(const auto& media : mc.collection) {
        media->displayInfo();
    }
    return os;
}

int main() {
    // Create collection
    MediaCollection myCollection("My Favorites");
    
    // Add items using factory
    myCollection += Media::createMedia('M', "Inception", 148);
    myCollection += Media::createMedia('S', "Hey Jude", 4);
    myCollection += Media::createMedia('M', "Interstellar", 169);
    
    // Display collection
    cout << myCollection << endl;
    
    // Play all
    myCollection.playAll();
    
    // Access specific item
    cout << "\nAccessing item 1:" << endl;
    myCollection[1].displayInfo();
    
    // Destructors called automatically when collection goes out of scope
    // All virtual destructors called correctly
    
    return 0;
}

This comprehensive guide covers advanced OOP concepts in C++ including operator overloading, friend functions/classes, and virtual destructors. These features enable you to create more expressive, safe, and efficient C++ code.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?