OOP Principles

Object-Oriented Programming (OOP)

DIFFICULTY: Intermediate READ TIME: 20 mins

Hey! Welcome back. Today, we’re diving into one of the most important concepts in modern software development: Object-Oriented Programming (OOP).

You might have heard OOP talked about as this dry, academic monster, but it’s actually a super intuitive way to structure code. Instead of writing long lists of sequential instructions, you organize your programs around “objects” that represent real-world concepts.


Classes and Objects: The Blueprints and Instances

The entire foundation of OOP rests on two things: Classes and Objects.

  • Class: A blueprint or template that defines the structure (data variables) and behavior (functions) of something. It doesn’t occupy memory itself—it’s just a design.
  • Object: A real-world instance of that class. When you create an object, C++ allocates memory for it on the stack or heap based on the blueprint.

Think of a Class like a house blueprint. The blueprint specifies where the walls, doors, and windows go, but you can’t live in a blueprint. An Object is the actual physical house built using that blueprint. You can build 50 identical houses from a single blueprint!

graph TD
    class_blueprint["Class: Student <br/> Blueprint (No Memory Allocation)"]
    class_blueprint -->|Instantiates| obj1["Object: s1 <br/> (Occupies Stack Memory) <br/> name: 'Aashutosh' <br/> rollNumber: 101"]
    class_blueprint -->|Instantiates| obj2["Object: s2 <br/> (Occupies Stack Memory) <br/> name: 'Swagata' <br/> rollNumber: 102"]

Class and Object Syntax

Here is how we define a class in C++ and instantiate an object from it. Notice the dot (.) operator used to access member variables and functions:

Example:

#include <iostream>
#include <string>

// Defining the blueprint
class Student {
public:
    std::string name;
    int rollNumber;

    void displayDetails() {
        std::cout << "Student Name: " << name << ", Roll Number: " << rollNumber << std::endl;
    }
};

int main() {
    // Creating instances (objects) of Student
    Student s1;
    s1.name = "Aashutosh";
    s1.rollNumber = 101;

    Student s2;
    s2.name = "Swagata";
    s2.rollNumber = 102;

    // Accessing methods
    s1.displayDetails();
    s2.displayDetails();

    return 0;
}

Output:

Student Name: Aashutosh, Roll Number: 101
Student Name: Swagata, Roll Number: 102

Access Specifiers: Controlling Visibility

Not everyone should be allowed to modify the inner workings of your objects. C++ provides three access specifiers to control who can view or modify class members:

  1. public: Members are accessible from anywhere in your codebase.
  2. private: Members can only be accessed by functions belonging to the same class. (This is the default if you don’t specify anything).
  3. protected: Members are private to the outside world, but can be accessed by derived (child) classes.
graph TD
    subgraph Inside Class
        priv[Private members: Accessible only within class functions]
        prot[Protected members: Accessible within class & child classes]
    end
    subgraph Outside World
        pub[Public members: Accessible anywhere via the dot operator]
    end

Access Permission Reference

Access SpecifierSame Class FunctionsFriend Functions/ClassesDerived Class FunctionsOutside Code
publicYesYesYesYes
protectedYesYesYesNo
privateYesYesNoNo

Compiling Private Members

If you try to touch a private member directly from main(), the compiler will throw an error immediately:

Example (Fails Compilation):

#include <iostream>

class Vault {
private:
    int secretKey = 9999;
};

int main() {
    Vault v;
    // std::cout << v.secretKey << std::endl; // ERROR! secretKey is private.
    return 0;
}

Member Functions: Inside vs. Outside

You can define member functions in two ways:

  1. Inside the class: Convenient for short, simple methods.
  2. Outside the class: Keeps the class blueprint clean and readable. You declare the method prototype inside the class, and implement it outside using the Scope Resolution Operator (::).

Example:

#include <iostream>

class Calculator {
public:
    // Defined inside the class
    int add(int x, int y) {
        return x + y;
    }

    // Declared inside, defined outside
    int subtract(int x, int y);
};

// Implement outside using Scope Resolution
int Calculator::subtract(int x, int y) {
    return x - y;
}

int main() {
    Calculator calc;
    std::cout << "Sum: " << calc.add(10, 5) << std::endl;
    std::cout << "Difference: " << calc.subtract(10, 5) << std::endl;
    return 0;
}

Output:

Sum: 15
Difference: 5

Instantiation During Declaration

In C++, you can declare an object instance directly after closing the class declaration block (before the semicolon):

Example:

#include <iostream>

class Printer {
public:
    void printHello() {
        std::cout << "Hello from the inline instance!" << std::endl;
    }
} myPrinter; // Instance created right here!

int main() {
    myPrinter.printHello();
    return 0;
}

Output:

Hello from the inline instance!

Constructors and Destructors

Constructors

A constructor is a special member function that gets called automatically the exact moment an object is created. It has the same name as the class and has no return type.

Types of Constructors:

  1. Default Constructor: Takes no arguments (or has default arguments). If you don’t write any constructor, C++ generates a basic one for you.
  2. Parameterized Constructor: Takes parameters to initialize member variables during instantiation.
  3. Copy Constructor: Creates a new object by cloning an existing object’s values.
  4. Move Constructor: Transfers ownership of heap-allocated resources from an expiring source object to a new object without performing expensive deep-copies.
graph TD
    subgraph Move Constructor Pointer Transfer
        direction LR
        subgraph Before Move
            src[Source Object] -->|Points to heap address| addr[0x8a92f0]
            dest[Dest Object] -->|Points to| null1[nullptr]
        end
        subgraph After Move
            src_after[Source Object] -->|Points to| null2[nullptr]
            dest_after[Dest Object] -->|Takes over heap address| addr
        end
    end

Let’s see all four constructors and a destructor in code:

Example:

#include <iostream>
#include <string>
#include <utility>

class User {
private:
    std::string* name;

public:
    // 1. Default Constructor
    User() {
        name = new std::string("Anonymous");
        std::cout << "Default constructor called." << std::endl;
    }

    // 2. Parameterized Constructor
    User(const std::string& userName) {
        name = new std::string(userName);
        std::cout << "Parameterized constructor called for: " << *name << std::endl;
    }

    // 3. Copy Constructor (Deep Copy)
    User(const User& other) {
        name = new std::string(*(other.name));
        std::cout << "Copy constructor called (Deep copied: " << *name << ")." << std::endl;
    }

    // 4. Move Constructor (Resource Handover)
    User(User&& other) noexcept : name(other.name) {
        other.name = nullptr; // Reset source pointer to avoid double-deletion
        std::cout << "Move constructor called." << std::endl;
    }

    // Destructor
    ~User() {
        if (name != nullptr) {
            std::cout << "Destructor cleaning up: " << *name << std::endl;
            delete name;
        } else {
            std::cout << "Destructor skipped (null resource pointer)." << std::endl;
        }
    }

    void display() {
        if (name != nullptr) {
            std::cout << "User: " << *name << std::endl;
        } else {
            std::cout << "User: [No resource]" << std::endl;
        }
    }
};

int main() {
    User u1; // Default
    User u2("Alice"); // Parameterized
    User u3 = u2; // Copy constructor
    
    std::cout << "Moving Alice to u4..." << std::endl;
    User u4 = std::move(u2); // Move constructor
    
    u4.display();
    u2.display(); // Alice resource is now gone!
    
    return 0;
}

Output:

Default constructor called.
Parameterized constructor called for: Alice
Copy constructor called (Deep copied: Alice).
Moving Alice to u4...
Move constructor called.
User: Alice
User: [No resource]
Destructor cleaning up: Alice
Destructor cleaning up: Alice
Destructor skipped (null resource pointer).
Destructor cleaning up: Anonymous

Encapsulation: Shielding Object State

Encapsulation is the practice of bundling data variables and the methods that modify them into a single class unit, and protecting them from direct external tampering using private access specifiers.

Think of it like a bank vault. You don’t walk inside and grab money yourself; you use an API (a bank teller) to deposit or withdraw. You access and modify private fields through public getters and setters.

Example:

#include <iostream>
#include <string>

class BankAccount {
private:
    std::string owner;
    double balance;

public:
    BankAccount(const std::string& accountOwner, double initialBalance) {
        owner = accountOwner;
        balance = initialBalance > 0 ? initialBalance : 0.0;
    }

    // Getter for Balance (Read-Only)
    double getBalance() const {
        return balance;
    }

    // Setter for Balance with Validation
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            std::cout << "Deposited $" << amount << ". New balance: $" << balance << std::endl;
        } else {
            std::cout << "Invalid deposit amount!" << std::endl;
        }
    }
};

int main() {
    BankAccount account("Aashutosh", 500.0);
    // account.balance = 999999.0; // ERROR! private variable.
    
    std::cout << "Current Balance: $" << account.getBalance() << std::endl;
    account.deposit(150.0);
    return 0;
}

Output:

Current Balance: $500
Deposited $150. New balance: $650

Data Abstraction: Exposing Interfaces

While encapsulation focuses on restricting access, Abstraction focuses on hiding complexity.

Think of it like driving a car. You step on the gas pedal to accelerate. You don’t need to know how fuel injection works, how the pistons fire, or how the differential splits torque to the axles. You interact with a clean dashboard interface.

In C++, abstraction is achieved via:

  1. Access Specifiers: Hiding internal helpers.
  2. Abstract Classes & Interfaces: Classes that have at least one pure virtual function (e.g. virtual void run() = 0;). Abstract classes cannot be instantiated; they serve as contracts for child classes to implement.

Example:

#include <iostream>

// Interface (Abstract Class)
class SmartAppliance {
public:
    virtual void turnOn() = 0; // Pure virtual function
    virtual void turnOff() = 0;
};

class SmartBulb : public SmartAppliance {
public:
    void turnOn() override {
        std::cout << "SmartBulb: Illuminating filament... brightness 100%." << std::endl;
    }
    
    void turnOff() override {
        std::cout << "SmartBulb: Powering down filament." << std::endl;
    }
};

int main() {
    // SmartAppliance app; // ERROR! Cannot instantiate abstract class.
    SmartBulb bulb;
    bulb.turnOn();
    bulb.turnOff();
    return 0;
}

Output:

SmartBulb: Illuminating filament... brightness 100%.
SmartBulb: Powering down filament.

Friend Classes: Breaking the Rules Safely

Usually, private attributes are locked away. But sometimes, two classes need to work together extremely closely. You can declare a class as a friend of another class, granting it full access to all private and protected members.

[!WARNING]

  • Friendship is not reciprocal: If Class A is friends with Class B, Class B is not automatically friends with Class A.
  • Friendship is not inherited: If Class A is friends with Class B, child classes of Class A do not inherit that friendship.

Example:

#include <iostream>
#include <string>

// Forward declaration
class SecretAgent;

class Vault {
private:
    std::string secretCode = "C++_MASTERY_2026";
    
    // Declare SecretAgent as a friend class
    friend class SecretAgent;
};

class SecretAgent {
public:
    void hackVault(const Vault& v) {
        // Accessing private attribute secretCode directly!
        std::cout << "Agent retrieved private code: " << v.secretCode << std::endl;
    }
};

int main() {
    Vault myVault;
    SecretAgent agent;
    agent.hackVault(myVault);
    return 0;
}

Output:

Agent retrieved private code: C++_MASTERY_2026

Inheritance: Sharing Attributes and Behaviors

Inheritance allows you to define a new class (derived/child) based on an existing class (base/parent). The child inherits all non-private fields and methods, reducing code duplication.

graph TD
    subgraph Inheritance Types
        direction TB
        subgraph Single
            s_b[Bike] --> s_d[Yamaha]
        end
        subgraph Multiple
            m_b1[Bullet] --> m_d[ElectricBullet]
            m_b2[ElectricBike] --> m_d
        end
        subgraph Multilevel
            ml_gp[Bike] --> ml_p[Bullet] --> ml_c[Bullet350Classic]
        end
        subgraph Hierarchical
            h_b[Bike] --> h_d1[Yamaha]
            h_b --> h_d2[Bullet]
        end
        subgraph Hybrid
            hy_p1[Bike] --> hy_p2[Bullet]
            hy_p3[ElectricBike]
            hy_p2 --> hy_c[HybridBullet]
            hy_p3 --> hy_c
        end
    end

Let’s implement these five types of inheritance in C++:

1. Single Inheritance

A child class inherits from exactly one parent class.

#include <iostream>

class Vehicle {
public:
    void move() { std::cout << "Vehicle is rolling..." << std::endl; }
};

class Car : public Vehicle {}; // inherits move()

int main() {
    Car c;
    c.move();
    return 0;
}

2. Multiple Inheritance

A child class inherits from more than one parent class.

#include <iostream>

class Engine {
public:
    void startEngine() { std::cout << "Engine VROOM!" << std::endl; }
};

class Battery {
public:
    void charge() { std::cout << "Battery charging..." << std::endl; }
};

// Child inherits from both Engine and Battery
class HybridCar : public Engine, public Battery {};

int main() {
    HybridCar hc;
    hc.startEngine();
    hc.charge();
    return 0;
}

3. Multilevel Inheritance

A child inherits from a parent, which inherits from a grandparent.

#include <iostream>

class Machine {
public:
    void powerOn() { std::cout << "Machine active." << std::endl; }
};

class Computer : public Machine {};
class Laptop : public Computer {}; // inherits from Computer

int main() {
    Laptop myLaptop;
    myLaptop.powerOn();
    return 0;
}

4. Hierarchical Inheritance

Multiple child classes inherit from the same parent.

#include <iostream>

class Animal {
public:
    void breathe() { std::cout << "Inhale, exhale..." << std::endl; }
};

class Dog : public Animal {};
class Cat : public Animal {};

int main() {
    Dog d;
    Cat c;
    d.breathe();
    c.breathe();
    return 0;
}

5. Hybrid Inheritance

A combination of two or more inheritance types.

#include <iostream>

class Device {
public:
    void boot() { std::cout << "Device booting..." << std::endl; }
};

class Mobile : public Device {};
class Tablet : public Device {};

// Combines Multiple and Hierarchical
class Phablet : public Mobile, public Tablet {};

int main() {
    Phablet p;
    // p.boot(); // ERROR! Ambiguous lookup: Device exists in both Mobile and Tablet branches.
    p.Mobile::boot(); // Resolved using scope resolution!
    return 0;
}

Polymorphism: One Interface, Many Forms

Polymorphism means “having multiple shapes.” In OOP, it allows different classes to respond to the same function call in unique ways.

There are two categories of polymorphism:

  1. Compile-time Polymorphism (Early Binding): Decided while the code is compiling.
    • Function Overloading: Same function name, different parameter signatures.
    • Operator Overloading: Customizing how operations (like +, -, <<) behave with user-defined objects.
  2. Runtime Polymorphism (Late Binding): Decided while the program is running.
    • Virtual Functions & Overriding: Subclasses redefine a virtual method defined in the parent class. It uses a Virtual Table (VTABLE) to dispatch the call to the actual object type at runtime.
graph LR
    base_ptr["Base Pointer: Shape* <br/> (points to Circle)"] -->|Dispatched at runtime| vtable["Circle VTABLE"]
    vtable -->|Invokes overridden| circ_draw["Circle::draw()"]

Operator Overloading Example

Let’s overload the + operator to add two complex number objects:

#include <iostream>

class Complex {
public:
    int real, imag;

    Complex(int r = 0, int i = 0) : real(r), imag(i) {}

    // Overloading the '+' operator
    Complex operator + (const Complex& other) {
        return Complex(real + other.real, imag + other.imag);
    }

    void print() {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

int main() {
    Complex c1(3, 4), c2(2, 5);
    Complex c3 = c1 + c2; // Calls custom operator+ function
    c3.print();
    return 0;
}

Output:

5 + 9i

Runtime Polymorphism Example (Virtual Methods)

Let’s demonstrate virtual functions. Without the virtual keyword in the base class, C++ defaults to static binding (calls parent class method instead of child method when using a parent pointer).

#include <iostream>

class Character {
public:
    // virtual keyword enables runtime lookup in the VTABLE
    virtual void attack() {
        std::cout << "Basic punch!" << std::endl;
    }
};

class Mage : public Character {
public:
    void attack() override {
        std::cout << "Cast Fireball!" << std::endl;
    }
};

class Knight : public Character {
public:
    void attack() override {
        std::cout << "Sword slash!" << std::endl;
    }
};

int main() {
    // Array of Base class pointers pointing to derived objects
    Character* party[2];
    party[0] = new Mage();
    party[1] = new Knight();

    // Loop through party and attack
    for (int i = 0; i < 2; ++i) {
        party[i]->attack(); // Runtime dispatch selects child methods!
    }

    // Clean up
    delete party[0];
    delete party[1];
    return 0;
}

Output:

Cast Fireball!
Sword slash!

Level Up: Extra Practice & Interview Questions

Q1. Why is Dynamic Memory Allocation necessary in OOP?

Dynamic memory allocation allows us to create objects whose size, count, or lifetime isn’t restricted by a local function scope or static limits. For instance, when loading user game profiles dynamically in an online multiplayer lobby, we can spawn and delete Player objects on the heap as they join and leave.


Q2. Write a parameterized constructor for a class Rectangle that initializes length and width.

We will use member initializer list syntax—it’s cleaner and runs more efficiently than standard assignment.

Code:

#include <iostream>

class Rectangle {
private:
    int length;
    int width;

public:
    // Parameterized constructor with initializer list
    Rectangle(int l, int w) : length(l), width(w) {}

    int getArea() {
        return length * width;
    }
};

int main() {
    Rectangle rect(10, 5);
    std::cout << "Rectangle Area: " << rect.getArea() << std::endl;
    return 0;
}

Output:

Rectangle Area: 50

Q3. Write a C++ class Temperature with a private member celsius and public methods to get/set Fahrenheit.

This exercise shows how encapsulation hides internal data representation while exposing a standard interface.

Code:

#include <iostream>

class Temperature {
private:
    float celsius;

public:
    Temperature(float initialC) : celsius(initialC) {}

    // Getter for Fahrenheit
    float getFahrenheit() const {
        return (celsius * 9.0 / 5.0) + 32.0;
    }

    // Setter for Fahrenheit
    void setFahrenheit(float f) {
        celsius = (f - 32.0) * 5.0 / 9.0;
    }
};

int main() {
    Temperature temp(25.0); // 25 C
    std::cout << "Fahrenheit: " << temp.getFahrenheit() << "F" << std::endl;
    
    temp.setFahrenheit(104.0); // 104 F (should be 40 C)
    std::cout << "Fahrenheit: " << temp.getFahrenheit() << "F" << std::endl;
    return 0;
}

Output:

Fahrenheit: 77F
Fahrenheit: 104F

Q4. What is the difference between public, private, and protected inheritance?

The visibility of inherited elements changes:

  • public inheritance: public and protected parent members keep their public and protected status in the child.
  • protected inheritance: public and protected parent members both become protected in the child.
  • private inheritance: public and protected parent members both become private in the child, blocking further inheritance chains.

Q5. Explain multilevel inheritance with a C++ example.

This illustrates a hierarchy chain where inheritance builds upon previous derivations.

Code:

#include <iostream>

class Animal {
public:
    void eat() { std::cout << "Eating food..." << std::endl; }
};

class Mammal : public Animal {
public:
    void breathe() { std::cout << "Breathing oxygen..." << std::endl; }
};

class Dog : public Mammal {
public:
    void bark() { std::cout << "Woof!" << std::endl; }
};

int main() {
    Dog myDog;
    myDog.eat();     // Inherited from Animal
    myDog.breathe(); // Inherited from Mammal
    myDog.bark();    // Defined in Dog
    return 0;
}

Output:

Eating food...
Breathing oxygen...
Woof!

Q6. What is function overloading? Provide a C++ example.

Function overloading allows a class to declare multiple methods with the exact same name as long as their parameters are different (either in data types or argument count).

Code:

#include <iostream>

class Display {
public:
    void show(int val) {
        std::cout << "Printing Integer: " << val << std::endl;
    }

    void show(double val) {
        std::cout << "Printing Double: " << val << std::endl;
    }

    void show(const char* val) {
        std::cout << "Printing String: " << val << std::endl;
    }
};

int main() {
    Display d;
    d.show(10);
    d.show(3.1415);
    d.show("Hello Overloading!");
    return 0;
}

Output:

Printing Integer: 10
Printing Double: 3.1415
Printing String: Hello Overloading!

Q7. What is operator overloading?

Operator overloading allows you to define custom behavior for operators (like +, -, *, ==, <<, etc.) when they are used with user-defined class objects. It makes user classes interact with standard operations naturally, matching built-in primitive syntax.


Q8. What is the output of the following code?

#include <iostream>

class Base {
public:
    virtual void show() {
        std::cout << "Base Class" << std::endl;
    }
};

class Derived : public Base {
public:
    void show() override {
        std::cout << "Derived Class" << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    ptr->show();
    delete ptr;
    return 0;
}

Answer:

Derived Class

Reason: Because the show() function in the Base class is declared as virtual, C++ uses runtime polymorphism (late binding). It resolves the function call using the VTABLE of the actual object type (Derived) that the pointer points to, rather than the pointer’s static type (Base*).

quiz Test Your Understanding

What is the default access specifier for class members in C++?