Object Oriented Programming

1. Basics of OOPs

Core Principles:

  • Encapsulation: Wrapping data (attributes) and methods (functions) together into a single unit, typically a class.

  • Inheritance: Enabling a new class to inherit properties and behavior from an existing class.

  • Polymorphism: Allowing entities to take multiple forms, typically achieved using function overloading, operator overloading, or overriding.

  • Abstraction: Hiding implementation details and exposing only the essential features.

Example: Encapsulation

#include <iostream>
using namespace std;

class Account {
private:
    double balance; // Encapsulated data

public:
    void setBalance(double amount) {
        if (amount >= 0) balance = amount;
        else cout << "Invalid amount!" << endl;
    }

    double getBalance() {
        return balance;
    }
};

int main() {
    Account acc;
    acc.setBalance(1000);
    cout << "Balance: " << acc.getBalance() << endl;
    return 0;
}

Practice:

  1. Implement a Car class with private attributes like speed and fuel. Add public methods to get and set these attributes.

  2. Create a Student class that stores a student's name, roll number, and marks. Write methods to calculate and return the grade.

2. Inheritance

Inheritance allows you to create a new class (child) that is based on an existing class (parent).

Example: Single Inheritance

class Vehicle {
public:
    void start() {
        cout << "Vehicle started!" << endl;
    }
};

class Car : public Vehicle {
public:
    void honk() {
        cout << "Car horn!" << endl;
    }
};

int main() {
    Car myCar;
    myCar.start(); // Inherited from Vehicle
    myCar.honk();  // Defined in Car
    return 0;
}

3. Polymorphism

Polymorphism can be achieved via:

  • Function Overloading: Same function name, different parameter list.

  • Operator Overloading: Redefine operators for custom behavior.

  • Function Overriding: Redefine a function in the derived class.

Example: Function Overloading

class Math {
public:
    int add(int a, int b) {
        return a + b;
    }

    double add(double a, double b) {
        return a + b;
    }
};

int main() {
    Math m;
    cout << m.add(5, 3) << endl;       // Calls int version
    cout << m.add(2.5, 3.5) << endl;  // Calls double version
    return 0;
}

4. Abstraction

Abstraction is implemented using:

  • Abstract Classes: Classes containing at least one pure virtual function.

  • An abstract class is created to serve as a blueprint for other classes. It allows you to define a common interface and enforce certain behaviors in derived classes while still providing flexibility for implementation

  • Interfaces: In C++, this is simulated using abstract classes with only pure virtual functions.

  • Virtual Functions and Virtual Tables (vTables)

    • Virtual functions allow dynamic (runtime) polymorphism.

    • The compiler maintains a table (vTable) to resolve calls to virtual functions at runtime.

Example:

Imagine you are designing a system for different types of shapes. You want all shapes to have a method for calculating their area.

class Shape {
public:
    virtual double area() = 0; // Pure virtual function
    virtual ~Shape() {} // Virtual destructor
};

class Circle : public Shape {
    double radius;

public:
    Circle(double r) : radius(r) {}
    double area() override {
        return 3.14 * radius * radius;
    }
};

class Rectangle : public Shape {
    double length, width;

public:
    Rectangle(double l, double w) : length(l), width(w) {}
    double area() override {
        return length * width;
    }
};

int main() {
    Shape* s1 = new Circle(5);
    Shape* s2 = new Rectangle(4, 6);

    cout << "Circle Area: " << s1->area() << endl;
    cout << "Rectangle Area: " << s2->area() << endl;

    delete s1;
    delete s2;
    return 0;
}

Virtual functions and pure virtual functions:

there is a significant difference between virtual functions and pure virtual functions in C++.

A virtual function is a member function in a base class that you expect to be overridden in derived classes. It allows you to achieve runtime polymorphism by using a base class pointer or reference to call derived class methods.

class Base {
public:
    virtual void display() {
        std::cout << "Display from Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void display() override {
        std::cout << "Display from Derived class" << std::endl;
    }
};

// Usage
Base* basePtr = new Derived();
basePtr->display();  // Output: Display from Derived class

In this example, the display function is a virtual function, allowing basePtr to call the Derived class’s display method at runtime.

A pure virtual function is a virtual function that is declared by assigning = 0 in its declaration. It makes the class an abstract class, meaning you cannot instantiate it directly, and it serves as a blueprint for derived classes.

Key Features:

  • Definition: Declared by assigning = 0 in the base class.

  • No Implementation: Does not have a body in the base class.

  • Mandatory Override: Must be overridden in derived classes, or the derived class will also become abstract.

  • Base Class Instantiation: Cannot instantiate a class with pure virtual functions.

class AbstractBase {
public:
    virtual void display() = 0;  // Pure virtual function
};

class Derived : public AbstractBase {
public:
    void display() override {
        std::cout << "Display from Derived class" << std::endl;
    }
};

// Usage
AbstractBase* basePtr = new Derived();
basePtr->display();  // Output: Display from Derived class

In this example, AbstractBase has a pure virtual function display, and Derived provides the actual implementation.

Copy Constructors

A copy constructor is a special constructor used to create a new object as a copy of an existing object. It is essential for classes that manage dynamic memory to ensure a deep copy is made when necessary.

Key Features:

  • Definition: A copy constructor takes a reference to an object of the same class as a parameter.

  • Default Behavior: The compiler provides a default copy constructor that performs a shallow copy.

  • Deep Copy: In cases involving dynamic memory, a user-defined copy constructor is needed to perform a deep copy.

class MyClass {
private:
    int* data;
public:
    MyClass(int value) {
        data = new int(value);
    }

    // Copy constructor for deep copy
    MyClass(const MyClass& other) {
        data = new int(*(other.data));
    }

    ~MyClass() {
        delete data;
    }
};

void copyExample() {
    MyClass obj1(10);
    MyClass obj2 = obj1;  // Copy constructor is called
}

SOLID Principals

The SOLID principles are a set of design principles in object-oriented programming that help developers create more maintainable, understandable, and flexible software. Each principle contributes to reducing the dependency and enhancing the reusability of the code.

1. S - Single Responsibility Principle (SRP)

A class should have only one reason to change, meaning it should have only one job or responsibility. This principle aims to separate concerns within the code.

Key Point: Each class should focus on a single task.

Example:

class Invoice {
public:
    void calculateTotal() {
        // Logic for calculating the total
    }
};
class InvoicePrinter {
public:
    void printInvoice(Invoice& invoice) {
        // Logic for printing the invoice
    }
}

In this example, the Invoice class handles calculations, while InvoicePrinter handles printing, adhering to SRP.

2. O - Open/Closed Principle (OCP)

Software entities (classes, modules, functions) should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.

Key Point: Extend a class’s behavior without modifying it.

Example:

class Shape {
public:
    virtual double area() = 0;
};

class Circle : public Shape {
public:
    double area() override {
        // Logic for circle area
        return 3.14 * radius * radius;
    }
};

class Rectangle : public Shape {
public:
    double area() override {
        // Logic for rectangle area
        return length * breadth;
    }
};

New shapes can be added by extending the Shape class without modifying existing code.

3. L - Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. Subclasses should fulfill the behavior expected from the superclass.

Key Point: Derived classes must be substitutable for their base classes.

class Bird {
public:
    virtual void fly() {}
};

class Sparrow : public Bird {
public:
    void fly() override {
        // Sparrow flying logic
    }
};

void makeBirdFly(Bird& bird) {
    bird.fly();  // Works for any subclass of Bird
}

Here, any subclass of Bird can be used in place of Bird without altering the functionality.

4. I - Interface Segregation Principle (ISP)

Clients should not be forced to implement interfaces they do not use. This principle suggests creating smaller, more specific interfaces rather than one large, general-purpose interface.

Key Point: Many client-specific interfaces are better than one general-purpose interface.

Example:

class Printer {
public:
    virtual void print() = 0;
};

class Scanner {
public:
    virtual void scan() = 0;
};

class MultiFunctionPrinter : public Printer, public Scanner {
public:
    void print() override {
        // Print logic
    }

    void scan() override {
        // Scan logic
    }
};

Here, Printer and Scanner interfaces are separate, adhering to ISP.

5. D - Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Key Point: Depend on abstractions, not on concretions.

Example:

class IKeyboard {
public:
    virtual void type() = 0;
};

class WiredKeyboard : public IKeyboard {
public:
    void type() override {
        // Wired keyboard typing logic
    }
};

class Computer {
private:
    IKeyboard& keyboard;
public:
    Computer(IKeyboard& k) : keyboard(k) {}

    void type() {
        keyboard.type();
    }
};

Here, Computer depends on the IKeyboard abstraction, not on a specific implementation like WiredKeyboard.

Summary

  • Single Responsibility Principle (SRP): A class should have only one reason to change.

  • Open/Closed Principle (OCP): Classes should be open for extension but closed for modification.

  • Liskov Substitution Principle (LSP): Subclasses should be replaceable by their base classes.

  • Interface Segregation Principle (ISP): Create specific interfaces instead of a general-purpose interface.

  • Dependency Inversion Principle (DIP): Depend on abstractions, not concrete implementations.

Design Patterns:

1. Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. It's often used to represent a single point of control, like a configuration manager or a logging class.

Key Features:

  • Single Instance: Only one instance of the class is created.

  • Global Access: Provides a global access point to the instance.

Implementation:

class Singleton {
private:
    static Singleton* instance;
    // Private constructor to prevent instantiation
    Singleton() {}
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};

// Initialize static member
Singleton* Singleton::instance = nullptr;

2. Factory Pattern

The Factory pattern provides a way to create objects without specifying the exact class of the object that will be created. It's useful when the creation process involves a complex logic, or when the exact type of object isn't known until runtime.

Key Features:

  • Encapsulation of Object Creation: Centralizes the creation of objects.

  • Flexibility: Allows the code to deal with interfaces rather than concrete classes.

Implementation:

class Product {
public:
    virtual void use() = 0;
};

class ConcreteProductA : public Product {
public:
    void use() override {
        std::cout << "Using Product A" << std::endl;
    }
};

class ConcreteProductB : public Product {
public:
    void use() override {
        std::cout << "Using Product B" << std::endl;
    }
};

class Factory {
public:
    static Product* createProduct(char type) {
        if (type == 'A') {
            return new ConcreteProductA();
        } else if (type == 'B') {
            return new ConcreteProductB();
        }
        return nullptr;
    }
};

// Usage
Product* product = Factory::createProduct('A');
product->use();

3. Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (observers) are notified and updated automatically. It's commonly used in event handling systems.

Key Features:

  • Subject: Maintains a list of observers and notifies them of state changes.

  • Observer: Defines an updating interface for objects that should be notified of changes in a subject.

Implementation:

#include <iostream>
#include <vector>
#include <algorithm>

class Observer {
public:
    virtual void update() = 0;
};

class Subject {
private:
    std::vector<Observer*> observers;
public:
    void addObserver(Observer* observer) {
        observers.push_back(observer);
    }

    void removeObserver(Observer* observer) {
        observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
    }

    void notifyObservers() {
        for (auto* observer : observers) {
            observer->update();
        }
    }
};

class ConcreteObserver : public Observer {
public:
    void update() override {
        std::cout << "Observer updated" << std::endl;
    }
};

// Usage
Subject subject;
ConcreteObserver observer;
subject.addObserver(&observer);
subject.notifyObservers();

Summary

  • Singleton: Ensures a class has only one instance.

  • Factory: Creates objects without specifying their exact class.

  • Observer: Defines a dependency between objects, allowing for notification of state changes