Object-Oriented Programming

Master the principles of OOP in C++

Introduction to OOP

Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects and classes rather than functions and logic. It models real-world entities as software objects that contain both data (attributes) and functions (methods) that operate on that data.

Why Object-Oriented Programming?

Code Reusability

Write once, use many times. Classes can be reused across different parts of your application and even in different projects.

Modularity

Break complex problems into smaller, manageable pieces. Each class handles a specific responsibility.

Maintainability

Easier to modify and update code. Changes in one class don't affect others when properly designed.

Real-World Modeling

Map real-world entities directly to code structures, making programs more intuitive and natural.

Four Pillars of OOP:

Encapsulation

Bundling data and methods that operate on that data within a single unit (class). It also involves data hiding - keeping internal details private.

Example: A car's engine is encapsulated - you don't need to know how it works internally, just how to start it.

Inheritance

Creating new classes based on existing classes, inheriting their properties and methods. Enables the "IS-A" relationship.

Example: A "Car" class can inherit from a "Vehicle" class, getting all vehicle properties plus car-specific features.

Polymorphism

The ability of objects to take multiple forms and behave differently based on context. "Many forms, one interface."

Example: Different animals make different sounds, but all respond to a "makeSound()" method call.

Abstraction

Hiding complex implementation details while showing only essential features. Focus on what an object does, not how it does it.

Example: When you use a TV remote, you don't need to know the electronics inside - just which buttons to press.

OOP vs Procedural Programming:

Object-Oriented Programming
Procedural Programming
Data and functions bundled together
Data and functions are separate
Data hiding through encapsulation
Global data accessible everywhere
Easy to maintain and extend
Difficult to maintain large programs
Code reusability through inheritance
Limited code reusability

Real-World OOP Examples:

School Management System

  • Classes: Student, Teacher, Course, Classroom
  • Inheritance: Person → Student, Teacher
  • Encapsulation: Student grades are private
  • Polymorphism: Different types of courses

Game Development

  • Classes: Player, Enemy, Weapon, GameObject
  • Inheritance: GameObject → Player, Enemy
  • Encapsulation: Player health and stats
  • Polymorphism: Different enemy behaviors

E-Commerce Platform

  • Classes: Product, Customer, Order, Payment
  • Inheritance: Payment → CreditCard, PayPal
  • Encapsulation: Customer personal data
  • Polymorphism: Different payment methods

Key Insight: OOP is not just about syntax - it's a way of thinking about problems. Start by identifying the "things" (objects) in your problem domain, then determine their properties and behaviors.

Ready to Begin: You've learned the basics of C++, now it's time to organize your code using Object-Oriented principles. Let's start building classes and objects!

Classes & Objects

A class is a blueprint or template for creating objects. An object is an instance of a class.

Class Syntax:

class ClassName {
private:
    // private members
public:
    // public members
};
class_example.cpp
#include <iostream>
#include <string>
using namespace std;

// Class definition
class Student {
private:
    string name;
    int age;
    double gpa;

public:
    // Constructor
    Student(string n, int a, double g) {
        name = n;
        age = a;
        gpa = g;
    }
    
    // Member functions (methods)
    void displayInfo() {
        cout << "Name: " << name << endl;
        cout << "Age: " << age << endl;
        cout << "GPA: " << gpa << endl;
    }
    
    // Getter methods
    string getName() { return name; }
    int getAge() { return age; }
    double getGPA() { return gpa; }
    
    // Setter methods
    void setName(string n) { name = n; }
    void setAge(int a) { age = a; }
    void setGPA(double g) { gpa = g; }
    
    // Method to check if student is honor roll
    bool isHonorRoll() {
        return gpa >= 3.5;
    }
};

int main() {
    // Creating objects (instances of the class)
    Student student1("Alice Johnson", 20, 3.8);
    Student student2("Bob Smith", 19, 3.2);
    
    // Using object methods
    cout << "Student 1 Information:" << endl;
    student1.displayInfo();
    cout << "Honor Roll: " << (student1.isHonorRoll() ? "Yes" : "No") << endl;
    cout << endl;
    
    cout << "Student 2 Information:" << endl;
    student2.displayInfo();
    cout << "Honor Roll: " << (student2.isHonorRoll() ? "Yes" : "No") << endl;
    
    // Modifying object data using setter methods
    student2.setGPA(3.7);
    cout << "\nAfter GPA update:" << endl;
    student2.displayInfo();
    
    return 0;
}

Key Concepts:

  • Class: A user-defined data type that serves as a blueprint
  • Object: An instance of a class with actual values
  • Member Variables: Data stored within the class
  • Member Functions: Functions that operate on the class data
  • Instantiation: The process of creating an object from a class

Constructors & Destructors

Constructors are special methods called when an object is created. Destructors are called when an object is destroyed.

Types of Constructors:

  • Default Constructor: Takes no parameters
  • Parameterized Constructor: Takes parameters to initialize object
  • Copy Constructor: Creates a copy of another object
constructors_example.cpp
#include <iostream>
#include <string>
using namespace std;

class Rectangle {
private:
    double length;
    double width;
    string color;

public:
    // Default constructor
    Rectangle() {
        length = 1.0;
        width = 1.0;
        color = "white";
        cout << "Default constructor called" << endl;
    }
    
    // Parameterized constructor
    Rectangle(double l, double w, string c) {
        length = l;
        width = w;
        color = c;
        cout << "Parameterized constructor called" << endl;
    }
    
    // Copy constructor
    Rectangle(const Rectangle &rect) {
        length = rect.length;
        width = rect.width;
        color = rect.color;
        cout << "Copy constructor called" << endl;
    }
    
    // Destructor
    ~Rectangle() {
        cout << "Destructor called for " << color << " rectangle" << endl;
    }
    
    // Member functions
    double getArea() {
        return length * width;
    }
    
    double getPerimeter() {
        return 2 * (length + width);
    }
    
    void displayInfo() {
        cout << "Rectangle - Length: " << length 
             << ", Width: " << width 
             << ", Color: " << color 
             << ", Area: " << getArea() << endl;
    }
};

int main() {
    cout << "Creating rectangle1 with default constructor:" << endl;
    Rectangle rectangle1;  // Default constructor
    rectangle1.displayInfo();
    
    cout << "\nCreating rectangle2 with parameterized constructor:" << endl;
    Rectangle rectangle2(5.0, 3.0, "blue");  // Parameterized constructor
    rectangle2.displayInfo();
    
    cout << "\nCreating rectangle3 with copy constructor:" << endl;
    Rectangle rectangle3 = rectangle2;  // Copy constructor
    rectangle3.displayInfo();
    
    cout << "\nProgram ending - destructors will be called:" << endl;
    return 0;
}

Constructor Initialization List:

A more efficient way to initialize member variables:

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

class Person {
private:
    const int id;  // const member must be initialized
    string name;
    int age;

public:
    // Constructor with initialization list
    Person(int i, string n, int a) : id(i), name(n), age(a) {
        cout << "Person created with ID: " << id << endl;
    }
    
    void displayInfo() {
        cout << "ID: " << id << ", Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    Person person1(101, "John Doe", 25);
    person1.displayInfo();
    
    return 0;
}

Important: Destructors are automatically called when objects go out of scope or are explicitly deleted. They're used for cleanup operations like freeing memory.

Access Modifiers

Access modifiers control the visibility and accessibility of class members.

Private

Members are accessible only within the same class

  • Default access level for class members
  • Provides data hiding
  • Accessed through public methods

Public

Members are accessible from anywhere

  • Can be accessed by any code
  • Forms the interface of the class
  • Should be used carefully

Protected

Members are accessible within the class and its derived classes

  • Used in inheritance
  • More restrictive than public
  • Less restrictive than private
access_modifiers_example.cpp
#include <iostream>
#include <string>
using namespace std;

class BankAccount {
private:
    double balance;        // Private - can't be accessed directly
    string accountNumber;  // Private - sensitive information
    
protected:
    string bankName;       // Protected - accessible to derived classes
    
public:
    string ownerName;      // Public - can be accessed directly
    
    // Constructor
    BankAccount(string owner, string accNum, double initialBalance) {
        ownerName = owner;
        accountNumber = accNum;
        balance = initialBalance;
        bankName = "Global Bank";
    }
    
    // Public methods to access private members
    double getBalance() {
        return balance;
    }
    
    string getAccountNumber() {
        return accountNumber;
    }
    
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            cout << "Deposited $" << amount << ". New balance: $" << balance << endl;
        }
    }
    
    bool withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            cout << "Withdrew $" << amount << ". New balance: $" << balance << endl;
            return true;
        } else {
            cout << "Invalid withdrawal amount or insufficient funds!" << endl;
            return false;
        }
    }
    
    void displayAccountInfo() {
        cout << "Account Owner: " << ownerName << endl;
        cout << "Account Number: " << accountNumber << endl;
        cout << "Bank: " << bankName << endl;
        cout << "Balance: $" << balance << endl;
    }
};

// Derived class to demonstrate protected access
class SavingsAccount : public BankAccount {
private:
    double interestRate;
    
public:
    SavingsAccount(string owner, string accNum, double initialBalance, double rate) 
        : BankAccount(owner, accNum, initialBalance) {
        interestRate = rate;
    }
    
    void addInterest() {
        double interest = getBalance() * interestRate / 100;
        deposit(interest);
        cout << "Interest added: $" << interest << endl;
    }
    
    void displayBankInfo() {
        // Can access protected member from base class
        cout << "This account is with: " << bankName << endl;
    }
};

int main() {
    BankAccount account1("John Smith", "ACC001", 1000.0);
    
    // Accessing public members
    cout << "Account owner: " << account1.ownerName << endl;
    
    // Accessing private members through public methods
    cout << "Balance: $" << account1.getBalance() << endl;
    
    // Using public methods
    account1.deposit(500.0);
    account1.withdraw(200.0);
    account1.displayAccountInfo();
    
    cout << "\n--- Savings Account ---" << endl;
    SavingsAccount savings("Jane Doe", "SAV001", 2000.0, 2.5);
    savings.displayAccountInfo();
    savings.addInterest();
    savings.displayBankInfo();  // Accessing protected member through derived class
    
    // The following would cause compilation errors:
    // cout << account1.balance;        // Error: private member
    // cout << account1.accountNumber;  // Error: private member
    // cout << account1.bankName;       // Error: protected member (not accessible here)
    
    return 0;
}

Best Practice: Keep data members private and provide public methods (getters/setters) to access them. This ensures data integrity and encapsulation.

Member Functions

Member functions are functions that belong to a class and operate on the class's data members. They define the behavior of objects.

Types of Member Functions:

Accessor Functions (Getters)

Functions that return the value of private data members

int getAge() const {
    return age;  // Return private member
}

Mutator Functions (Setters)

Functions that modify the value of private data members

void setAge(int newAge) {
    if (newAge >= 0) {
        age = newAge;
    }
}

Utility Functions

Functions that perform operations on the object's data

double calculateBMI() {
    return weight / (height * height);
}

Const Member Functions:

Functions that promise not to modify the object's state

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

class Circle {
private:
    double radius;
    
public:
    Circle(double r) : radius(r) {}
    
    // Const member function - doesn't modify the object
    double getArea() const {
        return 3.14159 * radius * radius;
    }
    
    // Const member function
    double getRadius() const {
        return radius;
    }
    
    // Non-const member function - can modify the object
    void setRadius(double r) {
        radius = r;
    }
    
    // Const member function that displays info
    void display() const {
        cout << "Circle with radius: " << radius 
             << ", Area: " << getArea() << endl;
    }
};

int main() {
    Circle circle(5.0);
    
    // Calling const member functions
    cout << "Radius: " << circle.getRadius() << endl;
    cout << "Area: " << circle.getArea() << endl;
    circle.display();
    
    // Modifying the object
    circle.setRadius(7.0);
    circle.display();
    
    // Const object can only call const member functions
    const Circle constCircle(3.0);
    cout << "Const circle area: " << constCircle.getArea() << endl;
    // constCircle.setRadius(4.0);  // Error! Cannot call non-const function
    
    return 0;
}

Best Practice: Mark member functions as const whenever they don't modify the object's state. This enables better optimization and allows const objects to use these functions.

Static Members

Static members belong to the class itself rather than to any specific object. They are shared among all instances of the class.

Static Data Members:

Class-level variables shared by all objects of the class

Static Member Functions:

Functions that can be called without creating an object of the class

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

class Student {
private:
    string name;
    int id;
    static int nextId;        // Static data member
    static int studentCount;  // Static data member
    
public:
    // Constructor
    Student(string n) : name(n) {
        id = nextId++;
        studentCount++;
        cout << "Student " << name << " created with ID: " << id << endl;
    }
    
    // Destructor
    ~Student() {
        studentCount--;
        cout << "Student " << name << " destroyed" << endl;
    }
    
    // Regular member function
    void displayInfo() const {
        cout << "Student: " << name << " (ID: " << id << ")" << endl;
    }
    
    // Static member function
    static int getStudentCount() {
        return studentCount;
    }
    
    // Static member function
    static int getNextId() {
        return nextId;
    }
    
    // Static member function to display class info
    static void displayClassInfo() {
        cout << "Total students created: " << studentCount << endl;
        cout << "Next ID will be: " << nextId << endl;
    }
};

// Initialize static members outside the class
int Student::nextId = 1001;
int Student::studentCount = 0;

int main() {
    // Call static function without creating object
    cout << "Initial student count: " << Student::getStudentCount() << endl;
    Student::displayClassInfo();
    cout << endl;
    
    // Create objects
    Student s1("Alice");
    Student s2("Bob");
    Student s3("Charlie");
    
    cout << "\nAfter creating students:" << endl;
    Student::displayClassInfo();
    cout << "Current student count: " << Student::getStudentCount() << endl;
    
    // Display individual student info
    cout << "\nStudent Information:" << endl;
    s1.displayInfo();
    s2.displayInfo();
    s3.displayInfo();
    
    {
        Student s4("David");
        cout << "\nAfter creating David:" << endl;
        cout << "Current student count: " << Student::getStudentCount() << endl;
    } // s4 goes out of scope here
    
    cout << "\nAfter David goes out of scope:" << endl;
    cout << "Current student count: " << Student::getStudentCount() << endl;
    
    return 0;
}

Key Points about Static Members:

Shared Data

Static data members are shared among all objects of the class

Single Copy

Only one copy exists in memory regardless of object count

Class Scope

Static functions can only access static members directly

External Access

Can be accessed using class name without object instance

Important: Static data members must be defined outside the class and initialized before use. Static functions cannot access non-static members directly.

Inheritance

Inheritance allows a class to inherit properties and methods from another class, promoting code reusability.

Types of Inheritance:

Single Inheritance

One derived class inherits from one base class

Multiple Inheritance

One derived class inherits from multiple base classes

Multilevel Inheritance

A derived class becomes base class for another class

Hierarchical Inheritance

Multiple derived classes inherit from one base class

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

// Base class (Parent class)
class Vehicle {
protected:
    string brand;
    string model;
    int year;
    
public:
    Vehicle(string b, string m, int y) : brand(b), model(m), year(y) {
        cout << "Vehicle constructor called" << endl;
    }
    
    void displayBasicInfo() {
        cout << "Brand: " << brand << endl;
        cout << "Model: " << model << endl;
        cout << "Year: " << year << endl;
    }
    
    virtual void start() {  // Virtual function for polymorphism
        cout << "Vehicle is starting..." << endl;
    }
    
    virtual ~Vehicle() {  // Virtual destructor
        cout << "Vehicle destructor called" << endl;
    }
};

// Derived class (Child class) - Single Inheritance
class Car : public Vehicle {
private:
    int doors;
    string fuelType;
    
public:
    Car(string b, string m, int y, int d, string fuel) 
        : Vehicle(b, m, y), doors(d), fuelType(fuel) {
        cout << "Car constructor called" << endl;
    }
    
    void displayCarInfo() {
        displayBasicInfo();  // Inherited method
        cout << "Doors: " << doors << endl;
        cout << "Fuel Type: " << fuelType << endl;
    }
    
    void start() override {  // Override base class method
        cout << "Car engine is starting with a key..." << endl;
    }
    
    void honk() {  // Car-specific method
        cout << "Beep beep!" << endl;
    }
    
    ~Car() {
        cout << "Car destructor called" << endl;
    }
};

// Another derived class - Single Inheritance
class Motorcycle : public Vehicle {
private:
    bool hasSidecar;
    
public:
    Motorcycle(string b, string m, int y, bool sidecar) 
        : Vehicle(b, m, y), hasSidecar(sidecar) {
        cout << "Motorcycle constructor called" << endl;
    }
    
    void displayMotorcycleInfo() {
        displayBasicInfo();
        cout << "Has Sidecar: " << (hasSidecar ? "Yes" : "No") << endl;
    }
    
    void start() override {
        cout << "Motorcycle is kick-starting..." << endl;
    }
    
    void wheelie() {
        cout << "Performing a wheelie!" << endl;
    }
    
    ~Motorcycle() {
        cout << "Motorcycle destructor called" << endl;
    }
};

// Multilevel Inheritance - SportsCar inherits from Car
class SportsCar : public Car {
private:
    int topSpeed;
    bool hasTurbo;
    
public:
    SportsCar(string b, string m, int y, int d, string fuel, int speed, bool turbo)
        : Car(b, m, y, d, fuel), topSpeed(speed), hasTurbo(turbo) {
        cout << "SportsCar constructor called" << endl;
    }
    
    void displaySportsCarInfo() {
        displayCarInfo();  // Inherited from Car
        cout << "Top Speed: " << topSpeed << " mph" << endl;
        cout << "Has Turbo: " << (hasTurbo ? "Yes" : "No") << endl;
    }
    
    void start() override {
        cout << "Sports car engine roaring to life!" << endl;
    }
    
    void activateTurbo() {
        if (hasTurbo) {
            cout << "Turbo activated! Maximum power!" << endl;
        } else {
            cout << "No turbo available." << endl;
        }
    }
    
    ~SportsCar() {
        cout << "SportsCar destructor called" << endl;
    }
};

int main() {
    cout << "=== Creating a Car ===" << endl;
    Car myCar("Toyota", "Camry", 2022, 4, "Gasoline");
    myCar.displayCarInfo();
    myCar.start();
    myCar.honk();
    
    cout << "\n=== Creating a Motorcycle ===" << endl;
    Motorcycle myBike("Harley-Davidson", "Street 750", 2021, false);
    myBike.displayMotorcycleInfo();
    myBike.start();
    myBike.wheelie();
    
    cout << "\n=== Creating a Sports Car ===" << endl;
    SportsCar mySportsCar("Ferrari", "488 GTB", 2023, 2, "Gasoline", 205, true);
    mySportsCar.displaySportsCarInfo();
    mySportsCar.start();
    mySportsCar.activateTurbo();
    
    cout << "\n=== Program ending - destructors will be called ===" << endl;
    return 0;
}

Access Specifiers in Inheritance:

Base Class Public Inheritance Protected Inheritance Private Inheritance
Public Public Protected Private
Protected Protected Protected Private
Private Not Accessible Not Accessible Not Accessible

Polymorphism

Polymorphism allows objects of different types to be treated as objects of a common base type, with the ability to call the appropriate method based on the actual object type.

Types of Polymorphism:

Compile-time Polymorphism

  • Function Overloading
  • Operator Overloading

Runtime Polymorphism

  • Virtual Functions
  • Function Overriding
polymorphism_example.cpp
#include <iostream>
#include <vector>
#include <memory>
using namespace std;

// Base class with virtual functions
class Shape {
protected:
    string color;
    
public:
    Shape(string c) : color(c) {}
    
    // Pure virtual function makes this an abstract class
    virtual double getArea() = 0;
    virtual double getPerimeter() = 0;
    
    // Virtual function with default implementation
    virtual void display() {
        cout << "This is a " << color << " shape" << endl;
    }
    
    // Virtual destructor
    virtual ~Shape() {
        cout << "Shape destructor called" << endl;
    }
};

// Derived class - Circle
class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(string c, double r) : Shape(c), radius(r) {}
    
    double getArea() override {
        return 3.14159 * radius * radius;
    }
    
    double getPerimeter() override {
        return 2 * 3.14159 * radius;
    }
    
    void display() override {
        cout << "Circle with radius " << radius << " and color " << color << endl;
    }
    
    ~Circle() {
        cout << "Circle destructor called" << endl;
    }
};

// Derived class - Rectangle
class Rectangle : public Shape {
private:
    double length, width;
    
public:
    Rectangle(string c, double l, double w) : Shape(c), length(l), width(w) {}
    
    double getArea() override {
        return length * width;
    }
    
    double getPerimeter() override {
        return 2 * (length + width);
    }
    
    void display() override {
        cout << "Rectangle " << length << "x" << width << " with color " << color << endl;
    }
    
    ~Rectangle() {
        cout << "Rectangle destructor called" << endl;
    }
};

// Derived class - Triangle
class Triangle : public Shape {
private:
    double side1, side2, side3;
    
public:
    Triangle(string c, double s1, double s2, double s3) 
        : Shape(c), side1(s1), side2(s2), side3(s3) {}
    
    double getArea() override {
        // Using Heron's formula
        double s = (side1 + side2 + side3) / 2;
        return sqrt(s * (s - side1) * (s - side2) * (s - side3));
    }
    
    double getPerimeter() override {
        return side1 + side2 + side3;
    }
    
    void display() override {
        cout << "Triangle with sides " << side1 << ", " << side2 
             << ", " << side3 << " and color " << color << endl;
    }
    
    ~Triangle() {
        cout << "Triangle destructor called" << endl;
    }
};

// Function demonstrating polymorphism
void printShapeInfo(Shape* shape) {
    shape->display();
    cout << "Area: " << shape->getArea() << endl;
    cout << "Perimeter: " << shape->getPerimeter() << endl;
    cout << "------------------------" << endl;
}

// Function overloading example (compile-time polymorphism)
class Calculator {
public:
    int add(int a, int b) {
        return a + b;
    }
    
    double add(double a, double b) {
        return a + b;
    }
    
    int add(int a, int b, int c) {
        return a + b + c;
    }
    
    string add(string a, string b) {
        return a + b;
    }
};

int main() {
    cout << "=== Runtime Polymorphism Example ===" << endl;
    
    // Create objects using smart pointers
    vector<unique_ptr<Shape>> shapes;
    shapes.push_back(make_unique<Circle>("red", 5.0));
    shapes.push_back(make_unique<Rectangle>("blue", 4.0, 6.0));
    shapes.push_back(make_unique<Triangle>("green", 3.0, 4.0, 5.0));
    
    // Polymorphic behavior - same interface, different implementations
    for (auto& shape : shapes) {
        printShapeInfo(shape.get());
    }
    
    cout << "\n=== Compile-time Polymorphism (Function Overloading) ===" << endl;
    Calculator calc;
    
    cout << "add(5, 3) = " << calc.add(5, 3) << endl;
    cout << "add(5.5, 3.2) = " << calc.add(5.5, 3.2) << endl;
    cout << "add(1, 2, 3) = " << calc.add(1, 2, 3) << endl;
    cout << "add(\"Hello\", \" World\") = " << calc.add(string("Hello"), string(" World")) << endl;
    
    return 0;
}

Key Point: Virtual functions enable runtime polymorphism, allowing the correct function to be called based on the actual object type, not the pointer type.

Virtual Functions

Virtual functions enable runtime polymorphism in C++. They allow a program to call the appropriate function based on the actual type of the object, not the type of the pointer or reference.

Types of Virtual Functions:

  • Virtual Function: Can be overridden in derived classes
  • Pure Virtual Function: Must be overridden in derived classes
  • Virtual Destructor: Ensures proper cleanup in inheritance hierarchies
virtual_functions_example.cpp
#include <iostream>
#include <vector>
#include <memory>
using namespace std;

// Base class with virtual functions
class Animal {
protected:
    string name;
    int age;

public:
    Animal(const string& animalName, int animalAge) : name(animalName), age(animalAge) {
        cout << "Animal constructor: " << name << endl;
    }
    
    // Virtual function - can be overridden
    virtual void makeSound() {
        cout << name << " makes a generic animal sound." << endl;
    }
    
    // Virtual function with default implementation
    virtual void move() {
        cout << name << " moves around." << endl;
    }
    
    // Pure virtual function - must be overridden
    virtual void eat() = 0;
    
    // Virtual function for displaying information
    virtual void displayInfo() {
        cout << "Name: " << name << ", Age: " << age << " years" << endl;
    }
    
    // Virtual destructor - important for proper cleanup
    virtual ~Animal() {
        cout << "Animal destructor: " << name << endl;
    }
};

class Dog : public Animal {
private:
    string breed;

public:
    Dog(const string& dogName, int dogAge, const string& dogBreed) 
        : Animal(dogName, dogAge), breed(dogBreed) {
        cout << "Dog constructor: " << name << endl;
    }
    
    // Override virtual function
    void makeSound() override {
        cout << name << " barks: Woof! Woof!" << endl;
    }
    
    // Override virtual function
    void move() override {
        cout << name << " runs around wagging its tail." << endl;
    }
    
    // Implement pure virtual function
    void eat() override {
        cout << name << " eats dog food and treats." << endl;
    }
    
    // Override and extend displayInfo
    void displayInfo() override {
        Animal::displayInfo();  // Call base class version
        cout << "Breed: " << breed << endl;
    }
    
    // Dog-specific method
    void fetch() {
        cout << name << " fetches the ball!" << endl;
    }
    
    ~Dog() {
        cout << "Dog destructor: " << name << endl;
    }
};

class Cat : public Animal {
private:
    bool isIndoor;

public:
    Cat(const string& catName, int catAge, bool indoor) 
        : Animal(catName, catAge), isIndoor(indoor) {
        cout << "Cat constructor: " << name << endl;
    }
    
    // Override virtual function
    void makeSound() override {
        cout << name << " meows: Meow! Meow!" << endl;
    }
    
    // Override virtual function
    void move() override {
        cout << name << " gracefully jumps and climbs." << endl;
    }
    
    // Implement pure virtual function
    void eat() override {
        cout << name << " eats cat food and fish." << endl;
    }
    
    // Override and extend displayInfo
    void displayInfo() override {
        Animal::displayInfo();
        cout << "Indoor cat: " << (isIndoor ? "Yes" : "No") << endl;
    }
    
    // Cat-specific method
    void purr() {
        cout << name << " purrs contentedly." << endl;
    }
    
    ~Cat() {
        cout << "Cat destructor: " << name << endl;
    }
};

class Bird : public Animal {
private:
    bool canFly;

public:
    Bird(const string& birdName, int birdAge, bool flying) 
        : Animal(birdName, birdAge), canFly(flying) {
        cout << "Bird constructor: " << name << endl;
    }
    
    // Override virtual function
    void makeSound() override {
        cout << name << " chirps: Tweet! Tweet!" << endl;
    }
    
    // Override virtual function
    void move() override {
        if (canFly) {
            cout << name << " flies through the sky." << endl;
        } else {
            cout << name << " hops around on the ground." << endl;
        }
    }
    
    // Implement pure virtual function
    void eat() override {
        cout << name << " eats seeds and insects." << endl;
    }
    
    // Override and extend displayInfo
    void displayInfo() override {
        Animal::displayInfo();
        cout << "Can fly: " << (canFly ? "Yes" : "No") << endl;
    }
    
    // Bird-specific method
    void buildNest() {
        cout << name << " builds a cozy nest." << endl;
    }
    
    ~Bird() {
        cout << "Bird destructor: " << name << endl;
    }
};

// Function demonstrating polymorphism with virtual functions
void animalActions(Animal* animal) {
    cout << "\n--- Animal Actions ---" << endl;
    animal->displayInfo();
    animal->makeSound();
    animal->move();
    animal->eat();
    cout << "----------------------" << endl;
}

// Demonstrating virtual function table (vtable) concept
void demonstrateVirtualFunctions() {
    cout << "\n=== Virtual Functions Demonstration ===" << endl;
    
    // Create animals using base class pointers
    vector> animals;
    animals.push_back(make_unique("Buddy", 3, "Golden Retriever"));
    animals.push_back(make_unique("Whiskers", 2, true));
    animals.push_back(make_unique("Robin", 1, true));
    
    // Polymorphic behavior - calls the appropriate derived class methods
    for (auto& animal : animals) {
        animalActions(animal.get());
    }
}

// Function without virtual functions (static binding)
class NonVirtualBase {
public:
    void show() {
        cout << "NonVirtualBase::show()" << endl;
    }
};

class NonVirtualDerived : public NonVirtualBase {
public:
    void show() {
        cout << "NonVirtualDerived::show()" << endl;
    }
};

void demonstrateNonVirtual() {
    cout << "\n=== Non-Virtual Functions (Static Binding) ===" << endl;
    
    NonVirtualBase* ptr = new NonVirtualDerived();
    ptr->show();  // Calls NonVirtualBase::show() - not what we might expect!
    
    delete ptr;
}

int main() {
    cout << "=== Virtual Functions Example ===" << endl;
    
    // Demonstrate virtual functions
    demonstrateVirtualFunctions();
    
    // Show the difference with non-virtual functions
    demonstrateNonVirtual();
    
    cout << "\n=== Program ending - destructors will be called ===" << endl;
    return 0;
}

Virtual Function Table (vtable):

Each class with virtual functions has a virtual function table (vtable) that contains pointers to the virtual functions. When a virtual function is called, the program looks up the correct function in the vtable.

How vtable works:

  1. Each object has a pointer to its class's vtable
  2. Virtual function calls are resolved at runtime
  3. The correct function is called based on the object's actual type

Rules for Virtual Functions:

  • Virtual functions cannot be static
  • Virtual functions cannot be inline
  • Constructors cannot be virtual
  • Destructors should be virtual in base classes
  • Pure virtual functions make the class abstract

Important: Always declare destructors as virtual in base classes to ensure proper cleanup when deleting objects through base class pointers.

Encapsulation

Encapsulation is the bundling of data and methods that operate on that data within a single unit, while restricting direct access to some of the object's components.

Benefits of Encapsulation:

  • Data Hiding: Internal representation is hidden from outside
  • Increased Security: Prevents unauthorized access to data
  • Easy Maintenance: Code changes don't affect other parts
  • Flexibility: Can change implementation without affecting users
encapsulation_example.cpp
#include <iostream>
#include <string>
using namespace std;

class Employee {
private:
    // Encapsulated data members
    int employeeId;
    string name;
    double salary;
    string department;
    bool isActive;

public:
    // Constructor
    Employee(int id, string empName, double sal, string dept) {
        employeeId = id;
        name = empName;
        salary = sal;
        department = dept;
        isActive = true;
    }
    
    // Public methods to access private data (Getters)
    int getEmployeeId() const { return employeeId; }
    string getName() const { return name; }
    double getSalary() const { return salary; }
    string getDepartment() const { return department; }
    bool getActiveStatus() const { return isActive; }
    
    // Public methods to modify private data (Setters) with validation
    void setName(const string& empName) {
        if (!empName.empty()) {
            name = empName;
        } else {
            cout << "Error: Name cannot be empty!" << endl;
        }
    }
    
    void setSalary(double sal) {
        if (sal >= 0) {
            salary = sal;
        } else {
            cout << "Error: Salary cannot be negative!" << endl;
        }
    }
    
    void setDepartment(const string& dept) {
        if (!dept.empty()) {
            department = dept;
        } else {
            cout << "Error: Department cannot be empty!" << endl;
        }
    }
    
    // Method to deactivate employee
    void deactivateEmployee() {
        isActive = false;
        cout << "Employee " << name << " has been deactivated." << endl;
    }
    
    // Method to give raise with business logic
    void giveRaise(double percentage) {
        if (percentage > 0 && percentage <= 50) {
            double oldSalary = salary;
            salary += salary * (percentage / 100);
            cout << name << "'s salary increased from $" << oldSalary 
                 << " to $" << salary << " (" << percentage << "% raise)" << endl;
        } else {
            cout << "Error: Invalid raise percentage!" << endl;
        }
    }
    
    // Method to display employee information
    void displayEmployeeInfo() const {
        cout << "Employee ID: " << employeeId << endl;
        cout << "Name: " << name << endl;
        cout << "Department: " << department << endl;
        cout << "Salary: $" << salary << endl;
        cout << "Status: " << (isActive ? "Active" : "Inactive") << endl;
        cout << "------------------------" << endl;
    }
};

// Demonstration of encapsulation
int main() {
    cout << "=== Encapsulation Example ===" << endl;
    
    Employee emp1(101, "John Smith", 50000, "Engineering");
    Employee emp2(102, "Jane Doe", 55000, "Marketing");
    
    // Display initial information
    cout << "Initial Employee Information:" << endl;
    emp1.displayEmployeeInfo();
    emp2.displayEmployeeInfo();
    
    // Using public methods to access and modify data
    cout << "Accessing data through public methods:" << endl;
    cout << "Employee 1 Name: " << emp1.getName() << endl;
    cout << "Employee 1 Salary: $" << emp1.getSalary() << endl;
    
    // Modifying data through setters (with validation)
    cout << "\nModifying employee data:" << endl;
    emp1.giveRaise(10);  // 10% raise
    emp2.setDepartment("Sales");
    
    // Trying to set invalid data (validation in action)
    cout << "\nTrying to set invalid data:" << endl;
    emp1.setSalary(-1000);  // This should show an error
    emp2.setName("");       // This should show an error
    
    // Final state
    cout << "\nFinal Employee Information:" << endl;
    emp1.displayEmployeeInfo();
    emp2.displayEmployeeInfo();
    
    // The following would cause compilation errors (encapsulation in action):
    // emp1.salary = 100000;        // Error: private member
    // emp1.employeeId = 999;       // Error: private member
    // cout << emp1.isActive;       // Error: private member
    
    return 0;
}

Best Practice: Always keep data members private and provide controlled access through public methods. This ensures data integrity and allows for validation.

Abstraction

Abstraction is the process of hiding complex implementation details while showing only the essential features of an object. It focuses on what an object does rather than how it does it.

Types of Abstraction:

Data Abstraction

Hiding the internal representation of data

Control Abstraction

Hiding the implementation details of functions

abstraction_example.cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;

// Abstract base class (interface)
class DatabaseConnection {
public:
    // Pure virtual functions (abstract methods)
    virtual bool connect(const string& connectionString) = 0;
    virtual bool disconnect() = 0;
    virtual bool executeQuery(const string& query) = 0;
    virtual vector<string> fetchResults() = 0;
    
    // Virtual destructor
    virtual ~DatabaseConnection() = default;
    
    // Common method with implementation
    void showConnectionStatus() {
        cout << "Database connection status checked." << endl;
    }
};

// Concrete implementation for MySQL
class MySQLConnection : public DatabaseConnection {
private:
    bool isConnected;
    string serverAddress;
    vector<string> queryResults;
    
    // Private helper method (hidden implementation detail)
    bool validateConnectionString(const string& connStr) {
        return !connStr.empty() && connStr.find("mysql://") == 0;
    }

public:
    MySQLConnection() : isConnected(false) {}
    
    bool connect(const string& connectionString) override {
        if (validateConnectionString(connectionString)) {
            serverAddress = connectionString;
            isConnected = true;
            cout << "Connected to MySQL database: " << serverAddress << endl;
            return true;
        }
        cout << "Failed to connect to MySQL database!" << endl;
        return false;
    }
    
    bool disconnect() override {
        if (isConnected) {
            isConnected = false;
            cout << "Disconnected from MySQL database." << endl;
            return true;
        }
        return false;
    }
    
    bool executeQuery(const string& query) override {
        if (!isConnected) {
            cout << "Error: Not connected to database!" << endl;
            return false;
        }
        
        cout << "Executing MySQL query: " << query << endl;
        
        // Simulate query execution and results
        queryResults.clear();
        queryResults.push_back("Result 1: MySQL data");
        queryResults.push_back("Result 2: MySQL data");
        
        return true;
    }
    
    vector<string> fetchResults() override {
        return queryResults;
    }
};

// Concrete implementation for PostgreSQL
class PostgreSQLConnection : public DatabaseConnection {
private:
    bool isConnected;
    string serverAddress;
    vector<string> queryResults;
    
    // Private helper method (different implementation)
    bool establishConnection(const string& connStr) {
        // PostgreSQL-specific connection logic
        return !connStr.empty() && connStr.find("postgresql://") == 0;
    }

public:
    PostgreSQLConnection() : isConnected(false) {}
    
    bool connect(const string& connectionString) override {
        if (establishConnection(connectionString)) {
            serverAddress = connectionString;
            isConnected = true;
            cout << "Connected to PostgreSQL database: " << serverAddress << endl;
            return true;
        }
        cout << "Failed to connect to PostgreSQL database!" << endl;
        return false;
    }
    
    bool disconnect() override {
        if (isConnected) {
            isConnected = false;
            cout << "Disconnected from PostgreSQL database." << endl;
            return true;
        }
        return false;
    }
    
    bool executeQuery(const string& query) override {
        if (!isConnected) {
            cout << "Error: Not connected to database!" << endl;
            return false;
        }
        
        cout << "Executing PostgreSQL query: " << query << endl;
        
        // Simulate query execution and results
        queryResults.clear();
        queryResults.push_back("Result 1: PostgreSQL data");
        queryResults.push_back("Result 2: PostgreSQL data");
        
        return true;
    }
    
    vector<string> fetchResults() override {
        return queryResults;
    }
};

// High-level database manager (uses abstraction)
class DatabaseManager {
private:
    DatabaseConnection* dbConnection;

public:
    DatabaseManager(DatabaseConnection* connection) : dbConnection(connection) {}
    
    void performDatabaseOperations(const string& connectionString) {
        // Client code doesn't need to know implementation details
        if (dbConnection->connect(connectionString)) {
            dbConnection->showConnectionStatus();
            
            // Execute a query
            if (dbConnection->executeQuery("SELECT * FROM users")) {
                vector<string> results = dbConnection->fetchResults();
                cout << "Query results:" << endl;
                for (const auto& result : results) {
                    cout << "- " << result << endl;
                }
            }
            
            dbConnection->disconnect();
        }
    }
    
    ~DatabaseManager() {
        delete dbConnection;
    }
};

int main() {
    cout << "=== Abstraction Example ===" << endl;
    
    // Using MySQL implementation
    cout << "\n--- Using MySQL Database ---" << endl;
    DatabaseManager mysqlManager(new MySQLConnection());
    mysqlManager.performDatabaseOperations("mysql://localhost:3306/mydb");
    
    // Using PostgreSQL implementation
    cout << "\n--- Using PostgreSQL Database ---" << endl;
    DatabaseManager postgresManager(new PostgreSQLConnection());
    postgresManager.performDatabaseOperations("postgresql://localhost:5432/mydb");
    
    // The client code (DatabaseManager) doesn't need to know
    // the specific implementation details of MySQL or PostgreSQL
    // It just works with the abstract interface
    
    return 0;
}

Abstract Classes vs Interfaces:

Abstract Class Interface (Pure Abstract Class)
Can have both abstract and concrete methods All methods are pure virtual (abstract)
Can have member variables Usually no member variables
Can have constructors Usually no constructors
Supports single inheritance in C++ Can simulate multiple inheritance

Remember: Abstraction helps in reducing complexity by hiding unnecessary details and showing only the relevant features to the user.

Operator Overloading

Operator overloading allows you to define custom behavior for operators when used with user-defined classes.

Why Operator Overloading?

It makes code more intuitive and readable. For example, adding two complex numbers with c1 + c2 instead of c1.add(c2).

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

class Complex {
private:
    double real;
    double imaginary;
    
public:
    // Constructor
    Complex(double r = 0.0, double i = 0.0) : real(r), imaginary(i) {}
    
    // Addition operator overloading
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imaginary + other.imaginary);
    }
    
    // Subtraction operator overloading
    Complex operator-(const Complex& other) const {
        return Complex(real - other.real, imaginary - other.imaginary);
    }
    
    // Multiplication operator overloading
    Complex operator*(const Complex& other) const {
        double r = real * other.real - imaginary * other.imaginary;
        double i = real * other.imaginary + imaginary * other.real;
        return Complex(r, i);
    }
    
    // Assignment operator overloading
    Complex& operator=(const Complex& other) {
        if (this != &other) {  // Self-assignment check
            real = other.real;
            imaginary = other.imaginary;
        }
        return *this;
    }
    
    // Equality operator overloading
    bool operator==(const Complex& other) const {
        return (real == other.real) && (imaginary == other.imaginary);
    }
    
    // Inequality operator overloading
    bool operator!=(const Complex& other) const {
        return !(*this == other);
    }
    
    // Unary minus operator
    Complex operator-() const {
        return Complex(-real, -imaginary);
    }
    
    // Pre-increment operator
    Complex& operator++() {
        real++;
        imaginary++;
        return *this;
    }
    
    // Post-increment operator
    Complex operator++(int) {
        Complex temp = *this;
        real++;
        imaginary++;
        return temp;
    }
    
    // Friend function for output stream operator
    friend ostream& operator<<(ostream& os, const Complex& c) {
        os << c.real;
        if (c.imaginary >= 0) {
            os << " + " << c.imaginary << "i";
        } else {
            os << " - " << (-c.imaginary) << "i";
        }
        return os;
    }
    
    // Friend function for input stream operator
    friend istream& operator>>(istream& is, Complex& c) {
        cout << "Enter real part: ";
        is >> c.real;
        cout << "Enter imaginary part: ";
        is >> c.imaginary;
        return is;
    }
    
    // Accessor methods
    double getReal() const { return real; }
    double getImaginary() const { return imaginary; }
};

int main() {
    Complex c1(3.0, 4.0);
    Complex c2(1.0, 2.0);
    
    cout << "c1 = " << c1 << endl;
    cout << "c2 = " << c2 << endl;
    
    // Using overloaded operators
    Complex c3 = c1 + c2;
    cout << "c1 + c2 = " << c3 << endl;
    
    Complex c4 = c1 - c2;
    cout << "c1 - c2 = " << c4 << endl;
    
    Complex c5 = c1 * c2;
    cout << "c1 * c2 = " << c5 << endl;
    
    // Unary operators
    Complex c6 = -c1;
    cout << "-c1 = " << c6 << endl;
    
    // Increment operators
    cout << "c1 before pre-increment: " << c1 << endl;
    ++c1;
    cout << "c1 after pre-increment: " << c1 << endl;
    
    Complex c7 = c2++;
    cout << "c2 after post-increment: " << c2 << endl;
    cout << "c7 (old value of c2): " << c7 << endl;
    
    // Comparison operators
    if (c1 == c2) {
        cout << "c1 and c2 are equal" << endl;
    } else {
        cout << "c1 and c2 are not equal" << endl;
    }
    
    // Input operator (commented out for automatic execution)
    // Complex userComplex;
    // cout << "Enter a complex number:" << endl;
    // cin >> userComplex;
    // cout << "You entered: " << userComplex << endl;
    
    return 0;
}

Types of Operator Overloading:

Member Functions

  • Binary operators: +, -, *, /, ==, !=
  • Unary operators: ++, --, -, !
  • Assignment operators: =, +=, -=

Friend Functions

  • Stream operators: <<, >>
  • When left operand is not of class type
  • When you need access to private members

Note: Some operators cannot be overloaded: ::, ., .*, ?:, sizeof, typeid

Friend Functions

Friend functions are non-member functions that have access to the private and protected members of a class.

When to Use Friend Functions:

Operator Overloading

When the left operand is not of the class type

Bridge Functions

Functions that need to access private data of multiple classes

Stream Operations

Input/output stream operators (<<, >>)

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

class Rectangle; // Forward declaration

class Point {
private:
    int x, y;
    
public:
    Point(int xPos = 0, int yPos = 0) : x(xPos), y(yPos) {}
    
    // Friend function declaration
    friend void displayPoint(const Point& p);
    friend double distance(const Point& p1, const Point& p2);
    friend bool isInsideRectangle(const Point& p, const Rectangle& r);
    
    // Getters for demonstration
    int getX() const { return x; }
    int getY() const { return y; }
};

class Rectangle {
private:
    Point topLeft;
    Point bottomRight;
    
public:
    Rectangle(Point tl, Point br) : topLeft(tl), bottomRight(br) {}
    
    // Friend function declaration
    friend bool isInsideRectangle(const Point& p, const Rectangle& r);
    friend void displayRectangle(const Rectangle& r);
    
    // Regular member function
    int getWidth() const {
        return bottomRight.getX() - topLeft.getX();
    }
    
    int getHeight() const {
        return bottomRight.getY() - topLeft.getY();
    }
};

// Friend function implementations
void displayPoint(const Point& p) {
    // Can access private members directly
    cout << "Point(" << p.x << ", " << p.y << ")" << endl;
}

double distance(const Point& p1, const Point& p2) {
    // Can access private members of both points
    int dx = p1.x - p2.x;
    int dy = p1.y - p2.y;
    return sqrt(dx*dx + dy*dy);
}

bool isInsideRectangle(const Point& p, const Rectangle& r) {
    // Can access private members of both classes
    return (p.x >= r.topLeft.x && p.x <= r.bottomRight.x &&
            p.y >= r.topLeft.y && p.y <= r.bottomRight.y);
}

void displayRectangle(const Rectangle& r) {
    // Can access private members
    cout << "Rectangle: TopLeft";
    displayPoint(r.topLeft);
    cout << "           BottomRight";
    displayPoint(r.bottomRight);
    cout << "           Width: " << r.bottomRight.x - r.topLeft.x
         << ", Height: " << r.bottomRight.y - r.topLeft.y << endl;
}

// Global friend function for demonstration
class Temperature {
private:
    double celsius;
    
public:
    Temperature(double c = 0.0) : celsius(c) {}
    
    // Friend function for conversion
    friend Temperature fahrenheitToCelsius(double f);
    friend double celsiusToFahrenheit(const Temperature& t);
    
    void display() const {
        cout << celsius << "°C" << endl;
    }
};

Temperature fahrenheitToCelsius(double f) {
    Temperature temp;
    temp.celsius = (f - 32.0) * 5.0 / 9.0;
    return temp;
}

double celsiusToFahrenheit(const Temperature& t) {
    return (t.celsius * 9.0 / 5.0) + 32.0;
}

int main() {
    // Using friend functions with Point and Rectangle
    Point p1(5, 10);
    Point p2(8, 15);
    
    cout << "=== Point Operations ===" << endl;
    cout << "Point 1: ";
    displayPoint(p1);
    cout << "Point 2: ";
    displayPoint(p2);
    cout << "Distance between points: " << distance(p1, p2) << endl;
    
    // Rectangle operations
    Point topLeft(0, 0);
    Point bottomRight(10, 20);
    Rectangle rect(topLeft, bottomRight);
    
    cout << "\n=== Rectangle Operations ===" << endl;
    displayRectangle(rect);
    
    // Check if points are inside rectangle
    cout << "\nPoint inside rectangle check:" << endl;
    cout << "Point 1 inside rectangle: " 
         << (isInsideRectangle(p1, rect) ? "Yes" : "No") << endl;
    cout << "Point 2 inside rectangle: " 
         << (isInsideRectangle(p2, rect) ? "Yes" : "No") << endl;
    
    // Temperature conversion example
    cout << "\n=== Temperature Conversion ===" << endl;
    Temperature temp1(25.0);
    cout << "Temperature 1: ";
    temp1.display();
    cout << "In Fahrenheit: " << celsiusToFahrenheit(temp1) << "°F" << endl;
    
    Temperature temp2 = fahrenheitToCelsius(98.6);
    cout << "98.6°F in Celsius: ";
    temp2.display();
    
    return 0;
}

Friend Classes:

A class can also be declared as a friend, giving all its member functions access to private members.

class ClassA {
    friend class ClassB;  // ClassB is a friend of ClassA
private:
    int privateData;
public:
    ClassA(int data) : privateData(data) {}
};

class ClassB {
public:
    void accessPrivateData(ClassA& a) {
        // Can access private members of ClassA
        cout << "Private data: " << a.privateData << endl;
    }
};

Important: Friendship is not mutual, inherited, or transitive. If A is a friend of B, B is not automatically a friend of A.

Composition & Aggregation

Composition and Aggregation are ways to combine objects to create more complex structures. They represent "HAS-A" relationships.

Composition vs Aggregation:

Composition (Strong "Has-A")

Ownership: Parent owns child completely

Lifetime: Child cannot exist without parent

Example: House has Rooms - rooms don't exist without the house

Aggregation (Weak "Has-A")

Ownership: Parent uses child but doesn't own it

Lifetime: Child can exist independently

Example: University has Students - students exist without university

Composition Example:

composition_example.cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;

// Composition Example: Engine belongs to Car
class Engine {
private:
    string type;
    int horsepower;
    
public:
    Engine(string t, int hp) : type(t), horsepower(hp) {
        cout << "Engine created: " << type << " (" << horsepower << " HP)" << endl;
    }
    
    ~Engine() {
        cout << "Engine destroyed: " << type << endl;
    }
    
    void start() {
        cout << type << " engine started!" << endl;
    }
    
    void stop() {
        cout << type << " engine stopped!" << endl;
    }
    
    void displayInfo() const {
        cout << "Engine: " << type << " - " << horsepower << " HP" << endl;
    }
};

class Tire {
private:
    string brand;
    int size;
    
public:
    Tire(string b, int s) : brand(b), size(s) {
        cout << "Tire created: " << brand << " (Size " << size << ")" << endl;
    }
    
    ~Tire() {
        cout << "Tire destroyed: " << brand << endl;
    }
    
    void displayInfo() const {
        cout << "Tire: " << brand << " - Size " << size << endl;
    }
};

class Car {
private:
    string model;
    Engine engine;           // Composition: Car owns Engine
    vector<Tire> tires;      // Composition: Car owns Tires
    
public:
    // Constructor creates engine and tires
    Car(string m, string engineType, int hp) 
        : model(m), engine(engineType, hp) {
        // Create 4 tires
        for (int i = 0; i < 4; ++i) {
            tires.emplace_back("Michelin", 18);
        }
        cout << "Car created: " << model << endl;
    }
    
    ~Car() {
        cout << "Car destroyed: " << model << endl;
        // Engine and tires are automatically destroyed
    }
    
    void start() {
        cout << "Starting " << model << "..." << endl;
        engine.start();
    }
    
    void stop() {
        cout << "Stopping " << model << "..." << endl;
        engine.stop();
    }
    
    void displayInfo() const {
        cout << "\n=== Car Information ===" << endl;
        cout << "Model: " << model << endl;
        engine.displayInfo();
        cout << "Tires (" << tires.size() << "):" << endl;
        for (const auto& tire : tires) {
            tire.displayInfo();
        }
    }
};

int main() {
    cout << "=== Composition Example ===" << endl;
    {
        Car myCar("Honda Civic", "VTEC", 180);
        myCar.displayInfo();
        myCar.start();
        myCar.stop();
    } // Car, engine, and tires are all destroyed here
    
    return 0;
}

Aggregation Example:

aggregation_example.cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;

// Aggregation Example: Students exist independently of University
class Student {
private:
    string name;
    int id;
    string major;
    
public:
    Student(string n, int i, string m) : name(n), id(i), major(m) {
        cout << "Student created: " << name << " (ID: " << id << ")" << endl;
    }
    
    ~Student() {
        cout << "Student destroyed: " << name << endl;
    }
    
    void displayInfo() const {
        cout << "Student: " << name << " (ID: " << id << ") - Major: " << major << endl;
    }
    
    string getName() const { return name; }
    int getId() const { return id; }
    string getMajor() const { return major; }
};

class Professor {
private:
    string name;
    string department;
    
public:
    Professor(string n, string d) : name(n), department(d) {
        cout << "Professor created: " << name << " (" << department << ")" << endl;
    }
    
    ~Professor() {
        cout << "Professor destroyed: " << name << endl;
    }
    
    void displayInfo() const {
        cout << "Professor: " << name << " - Department: " << department << endl;
    }
    
    string getName() const { return name; }
    string getDepartment() const { return department; }
};

class University {
private:
    string name;
    vector<Student*> students;    // Aggregation: University uses Students
    vector<Professor*> professors; // Aggregation: University uses Professors
    
public:
    University(string n) : name(n) {
        cout << "University created: " << name << endl;
    }
    
    ~University() {
        cout << "University destroyed: " << name << endl;
        // Note: We don't delete students/professors - they exist independently
    }
    
    void addStudent(Student* student) {
        students.push_back(student);
        cout << "Student " << student->getName() << " enrolled in " << name << endl;
    }
    
    void addProfessor(Professor* professor) {
        professors.push_back(professor);
        cout << "Professor " << professor->getName() << " joined " << name << endl;
    }
    
    void removeStudent(int studentId) {
        for (auto it = students.begin(); it != students.end(); ++it) {
            if ((*it)->getId() == studentId) {
                cout << "Student " << (*it)->getName() << " left " << name << endl;
                students.erase(it);
                break;
            }
        }
    }
    
    void displayInfo() const {
        cout << "\n=== " << name << " Information ===" << endl;
        cout << "Students (" << students.size() << "):" << endl;
        for (const auto& student : students) {
            student->displayInfo();
        }
        
        cout << "\nProfessors (" << professors.size() << "):" << endl;
        for (const auto& professor : professors) {
            professor->displayInfo();
        }
    }
};

int main() {
    cout << "=== Aggregation Example ===" << endl;
    
    // Create students and professors independently
    Student* alice = new Student("Alice Johnson", 1001, "Computer Science");
    Student* bob = new Student("Bob Smith", 1002, "Mathematics");
    Professor* drJones = new Professor("Dr. Jones", "Computer Science");
    Professor* drBrown = new Professor("Dr. Brown", "Mathematics");
    
    {
        // Create university and add existing students/professors
        University myUniversity("Tech University");
        myUniversity.addStudent(alice);
        myUniversity.addStudent(bob);
        myUniversity.addProfessor(drJones);
        myUniversity.addProfessor(drBrown);
        
        myUniversity.displayInfo();
        
        // Remove a student
        myUniversity.removeStudent(1002);
        
    } // University is destroyed, but students and professors still exist
    
    cout << "\nAfter university destruction:" << endl;
    alice->displayInfo();  // Alice still exists
    drJones->displayInfo(); // Dr. Jones still exists
    
    // Clean up (in real applications, use smart pointers)
    delete alice;
    delete bob;
    delete drJones;
    delete drBrown;
    
    return 0;
}

Key Difference: In composition, the parent manages the lifecycle of child objects. In aggregation, child objects exist independently and can outlive the parent.

OOP Design Principles

Good object-oriented design follows established principles that lead to maintainable, flexible, and robust software.

SOLID Principles:

S

Single Responsibility Principle

A class should have only one reason to change - it should have only one job or responsibility.

Example: A User class should only handle user data, not email sending or database operations.
O

Open/Closed Principle

Classes should be open for extension but closed for modification. Use inheritance and polymorphism.

Example: Add new shapes without modifying existing shape classes.
L

Liskov Substitution Principle

Objects of derived classes should be substitutable for objects of base classes without breaking functionality.

Example: Any Bird object should work wherever an Animal object is expected.
I

Interface Segregation Principle

Clients shouldn't depend on interfaces they don't use. Keep interfaces small and specific.

Example: Separate interfaces for different capabilities (Flyable, Swimmable).
D

Dependency Inversion Principle

High-level modules shouldn't depend on low-level modules. Both should depend on abstractions.

Example: Use abstract classes or interfaces instead of concrete implementations.

Design Patterns Preview:

Factory Pattern

Create objects without specifying exact classes

Observer Pattern

Notify multiple objects about state changes

Singleton Pattern

Ensure only one instance of a class exists

Strategy Pattern

Encapsulate algorithms and make them interchangeable

Best Practices Summary:

Encapsulation

  • Keep data members private
  • Provide public getters/setters with validation
  • Hide implementation details

Inheritance

  • Use "IS-A" relationship only
  • Prefer composition over inheritance when possible
  • Make destructors virtual in base classes

Polymorphism

  • Use virtual functions for runtime polymorphism
  • Implement pure virtual functions in abstract classes
  • Override functions properly with override keyword

General

  • Keep classes focused and cohesive
  • Use meaningful names for classes and methods
  • Write self-documenting code

Congratulations! You've mastered Object-Oriented Programming in C++! You can now design and implement complex software systems using OOP principles. Next, explore advanced C++ features to take your skills even further.