Object-Oriented Programming (OOP)
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:
public: Members are accessible from anywhere in your codebase.private: Members can only be accessed by functions belonging to the same class. (This is the default if you don’t specify anything).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 Specifier | Same Class Functions | Friend Functions/Classes | Derived Class Functions | Outside Code |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes | No |
private | Yes | Yes | No | No |
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:
- Inside the class: Convenient for short, simple methods.
- 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:
- Default Constructor: Takes no arguments (or has default arguments). If you don’t write any constructor, C++ generates a basic one for you.
- Parameterized Constructor: Takes parameters to initialize member variables during instantiation.
- Copy Constructor: Creates a new object by cloning an existing object’s values.
- 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:
- Access Specifiers: Hiding internal helpers.
- 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:
- 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.
- 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:
publicinheritance: public and protected parent members keep their public and protected status in the child.protectedinheritance: public and protected parent members both become protected in the child.privateinheritance: 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++?
By default, all members of a C++ class are private if no access specifier is explicitly declared.