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!