Introduction, Classes & Objects

What is Object-Oriented Programming (OOP)?

Object-Oriented Programming (OOP) is a programming paradigm that uses the concepts of class and object to model software. Popular object-oriented languages include C++, Java, Python, PHP and C#.

The main objective of OOP is to implement real-world entities in code and to build programs around four core ideas: encapsulation, abstraction, inheritance and polymorphism.

Procedural  -> functions operate on separate data
Object-oriented -> data + functions bundled together inside objects
Interview one-liner

OOP organises a program as a collection of objects that hold data and the behaviour that works on that data, created from classes.

Why do we use OOP?

  • It gives clarity in programming and makes complex problems simpler to solve.
  • Data and code are bound together by encapsulation.
  • Code can be reused (inheritance), which reduces redundancy.
  • Unnecessary details are hidden using data abstraction.
  • A large problem can be divided into smaller parts (classes/objects).
  • It improves readability, understandability and maintainability of code.

What is a class?

A class is a logical entity used to define a new, user-defined data type. It describes what a particular kind of object will look like, so a class is a template or blueprint for an object.

A class contains variables (data members), methods (member functions) and constructors.

Syntax

C++
class class_name {
    // class body
    // properties (data members)
    // methods (member functions)
};   // <-- semicolon is mandatory
Part Meaning
class Keyword used to create a class in C++
class_name The name of the class
class body Everything inside the curly braces { }
; A semicolon is required after the closing brace
Common mistake

Forgetting the semicolon after the closing brace of a class is a very common compile error in C++.

What is an object?

An object is an instance of a class. It is an identifiable entity with some characteristics (data) and behaviour (functions). Objects are the basic units of object-oriented programming, and may represent any real-world thing such as a person, chair, table, pen, animal or car.

You need a class before you can create an object. When a class is defined, no memory is allocated; memory is allocated only when the class is instantiated (an object is created).

Creating an object (stack)

C++
class_name objectName;

Creating an object dynamically (heap)

C++
class_name* objectName = new class_name();

The class's default constructor is called, and new dynamically allocates memory for one object. The address of that memory is stored in the pointer objectName.

C++
class Car {
public:
    int speed = 0;
};

int main() {
    Car c1;                 // stack object, destroyed automatically
    Car* c2 = new Car();    // heap object, you must free it

    c1.speed = 40;          // dot operator for objects
    c2->speed = 60;         // arrow operator for pointers

    delete c2;              // free heap memory
    return 0;
}
Remember

Stack objects are destroyed automatically when they go out of scope. Heap objects created with new must be released with delete.

Class vs object

class Car(blueprint) BMWspeed, colour Mercedesspeed, colour Ferrarispeed, colour instantiateinstantiate
Figure: one class (blueprint) can create many objects (instances).
Class Object
Blueprint of an object; used to create objects An instance of the class
No memory is allocated when a class is declared Memory is allocated as soon as an object is created
A group of similar objects A real-world entity such as a book or a car
A logical entity A physical entity
A class is declared only once Objects can be created many times as required
Example: Car Example: BMW, Mercedes, Ferrari

Anatomy of a class: data members, methods, constructor

The data in a class is called a member and the functions in a class are called methods.

Component Description
Data members Variables declared in a class using fundamental types (int, char, float) or derived types (class, structure, pointer)
Methods The equivalent of functions inside a class. They accept parameters, manipulate them, and produce output when called on an object
Constructor A special function that initialises every object. Called automatically at creation, has no return type and the same name as the class
C++
#include <iostream>
#include <string>
using namespace std;

class smartphone {
public:
    // Data members (properties)
    string model;
    int year_of_manufacture;
    bool _5g_supported;

    // Constructor
    smartphone(string mod, int manu, bool _5g_supp) {
        model = mod;
        year_of_manufacture = manu;
        _5g_supported = _5g_supp;
    }

    // Method
    void print_details() {
        cout << "Model : " << model << endl;
        cout << "Year of Manufacture : " << year_of_manufacture << endl;
        cout << "5g Supported : " << _5g_supported << endl;
    }
};

int main() {
    smartphone iphone("iphone 11", 2019, false);
    smartphone redmi("redmi note 11 t", 2021, true);

    // accessing data members
    int year = iphone.year_of_manufacture;
    string name = redmi.model;

    // calling methods on objects
    iphone.print_details();
    redmi.print_details();
    return 0;
}
Output:
Output
Model : iphone 11
Year of Manufacture : 2019
5g Supported : 0
Model : redmi note 11 t
Year of Manufacture : 2021
5g Supported : 1
Why `public:` is added here

Members of a class are private by default. If the constructor and data members are not placed under public:, smartphone iphone(...) and iphone.model will not compile when used from main().

Four features of OOP

Four major features make object-oriented languages different from non-OOP languages:

Feature Meaning
Abstraction Only the essential details are displayed to the user
Inheritance Creates class hierarchies where a base class passes behaviour and attributes to a derived class
Polymorphism The proper method is executed based on the calling object's type
Encapsulation Controls access to an object's state, making implementation easier to maintain or change later

Example: class Animal and objects mammal, amphibian, bird

C++
#include <iostream>
using namespace std;

class Animal {
public:
    bool gives_birth;
    bool lay_egg;
    bool live_in_ground;
    bool live_in_water;
    bool have_wings;
};

int main() {
    Animal mammal;
    mammal.gives_birth = true;
    mammal.lay_egg = false;
    mammal.live_in_ground = true;
    mammal.live_in_water = false;
    mammal.have_wings = false;

    Animal amphibian;
    amphibian.gives_birth = false;
    amphibian.lay_egg = true;
    amphibian.live_in_ground = true;
    amphibian.live_in_water = true;
    amphibian.have_wings = false;

    Animal bird;
    bird.gives_birth = false;
    bird.lay_egg = true;
    bird.live_in_ground = true;
    bird.live_in_water = false;
    bird.have_wings = true;
}
Compile note

The members must be public (or the class a struct) for main() to assign them directly. Otherwise the compiler reports that the member is private.

Class vs structure

Class: a user-defined blueprint from which objects are created. It consists of methods or sets of instructions that are performed on the objects.

Structure: a user-defined collection of variables of different data types.

Feature class struct
Default member access private public
Default inheritance mode private public
Typical use Objects with behaviour and hidden data Plain grouping of data
Can have methods, constructors, destructors Yes Yes (in C++)
Interview point

In C++ the only real technical difference between class and struct is the default access (and default inheritance) level.

Access Specifiers

What are access specifiers?

Access specifiers assign access levels to class members. They restrict outside functions from accessing members directly and therefore play a vital role in securing data from unauthorised access.

They let us decide which members are accessible to other classes and functions and which are not. C++ has three access specifiers:

  • public
  • private
  • protected
Default access

By default, in C++, all class members are private if you do not specify an access specifier.

public, private and protected

Specifier Who can access it
public Anywhere: inside the class, in child classes and outside the class
private Only member functions inside the same class
protected Inside the class and in child (derived) classes. Not accessible outside unless through a child class

Access summary

Access specifier Inside class Child (sub-class) Outside class
public ✔ ✔ ✔
protected ✔ ✔ ✘
private ✔ ✘ ✘
  public      -> everyone
  protected   -> class + derived classes
  private     -> class only

  (widest access)  public > protected > private  (narrowest access)

Access specifiers example

C++
class smartphone {
    // Data members
    string model;            // by default private

public:
    int year_of_manufacture; // public data member

protected:
    string company_name;     // protected data member

private:
    int password;            // private data member

    // Methods
private:
    void unlock_lockscreen() {
        // private method
    }

public:
    void call() {
        // public method
    }

protected:
    void about_phone() {
        // protected method
    }
};

What happens when you break the rules?

C++
int main() {
    smartphone s;
    s.year_of_manufacture = 2020;  // OK, public
    s.call();                      // OK, public
    s.password = 1234;             // error, private
    s.about_phone();               // error, protected
}
Compiler error:
Error
error: 'int smartphone::password' is private within this context
error: 'void smartphone::about_phone()' is protected within this context
Good practice

Keep data members private and expose only what is needed through public methods. This is the basis of encapsulation.

Constructors & Destructors

What is a constructor?

A constructor is a special member function that is called automatically when an object is created. It initialises the object's data members.

C++
class class_name {
    int data_member1;
    string data_member2;

public:
    class_name() {          // constructor
        // initialise data members
    }
};

Notice that the constructor:

  • has the same name as the class,
  • has no return type (not even void),
  • is normally placed in the public section so the class can be instantiated anywhere,
  • is called only once for each object, at the time of creation.
Garbage values

If you do not initialise members explicitly, built-in members such as int hold garbage (indeterminate) values.

If you write no constructor at all, the compiler generates a default constructor with an empty body and no parameters. Once you write any constructor yourself, the compiler-generated default one is no longer available.

Types of constructors

Type Parameters Purpose
Default constructor None Initialise an object with default values
Parameterized constructor One or more Initialise an object with values given by the programmer
Copy constructor A reference to an object of the same class Create a new object as a copy of an existing one
Constructors
 |-- Default        ->  ClassName()
 |-- Parameterized  ->  ClassName(int a, string b)
 \-- Copy           ->  ClassName(const ClassName& obj)

Default constructor

A constructor that takes no arguments is a default constructor. When an object is created, it sets the data members to the values written in its body.

C++
class class_name {
    int data_member1;
    string data_member2;

public:
    // default constructor
    class_name() {
        data_member1 = 69;
        data_member2 = "Coding Ninjas";
    }
};

Here data_member1 is set to 69 and data_member2 to "Coding Ninjas" whenever an object is created.

Compiler-generated default constructor

If no constructor is defined, the compiler creates one with empty code and no parameters. It leaves fundamental-type members uninitialised (garbage).

Parameterized constructor

A parameterized constructor takes arguments provided by the programmer. Add parameters just like in any function and use them to initialise the object. Different objects can therefore receive different values.

C++
class class_name {
    int data_member1;
    string data_member2;

public:
    // parameterized constructor
    class_name(int num, string str) {
        data_member1 = num;
        data_member2 = str;
    }
};

class_name obj(10, "Hello");
Member initializer list (preferred style)

Instead of assigning inside the body, you can initialise members directly:

C++
class_name(int num, string str)
    : data_member1(num), data_member2(str) {}

Copy constructor

A copy constructor takes an object of the same class as its argument and copies the values of that object's data members into the new object. We pass the object by reference (&).

C++
class class_name {
    int data_member1;
    string data_member2;

public:
    // copy constructor
    class_name(const class_name& obj) {
        data_member1 = obj.data_member1;
        data_member2 = obj.data_member2;
    }
};

If you do not define your own copy constructor, the compiler creates a default one that performs a member-wise copy between objects.

Why must the parameter be a reference?

If the parameter were passed by value, making that copy would itself call the copy constructor, which would call the copy constructor again, and so on: infinite recursion. Hence class_name(const class_name&).

Using all three constructors together

C++
class smartphone {
    string model;
    int year_of_manufacture;
    bool _5g_supported;

public:
    // Default constructor
    smartphone() {
        model = "unknown";
        year_of_manufacture = 0;
        _5g_supported = false;
    }

    // Parameterized constructor
    smartphone(string model_string, int manufacture, bool _5g_) {
        model = model_string;
        year_of_manufacture = manufacture;
        _5g_supported = _5g_;
    }

    // Copy constructor
    smartphone(const smartphone& obj) {
        model = obj.model;
        year_of_manufacture = obj.year_of_manufacture;
        _5g_supported = obj._5g_supported;
    }
};

int main() {
    smartphone unknown;                        // default
    smartphone iphone("iphone 11", 2019, false); // parameterized
    smartphone iphone_2(iphone);               // copy
}

When is the copy constructor called?

  • When an object is initialised from another object: A b(a); or A b = a;
  • When an object is passed by value to a function
  • When an object is returned by value from a function (may be elided by the compiler)

Constructor overloading

Constructor overloading means having more than one constructor with different parameters, so each constructor can perform a different task. To overload a constructor, differ the number or the type of arguments. The compiler selects the constructor that matches the arguments.

C++
class smartphone {
    string model;
    int year_of_manufacture;
    bool _5g_supported;

public:
    // 0 parameters
    smartphone() {
        model = "unknown";
        year_of_manufacture = 0;
        _5g_supported = false;
    }

    // 2 parameters
    smartphone(string model_string, bool _5g_) {
        model = model_string;
        _5g_supported = _5g_;
    }

    // 3 parameters
    smartphone(string model_string, int manufacture, bool _5g_) {
        model = model_string;
        year_of_manufacture = manufacture;
        _5g_supported = _5g_;
    }
};

int main() {
    smartphone unknown;                           // 0 parameters
    smartphone redmi("Note 7 Pro", false);        // 2 parameters
    smartphone iphone("iphone 11", 2019, false);  // 3 parameters
}
smartphone()                  <- no arguments
smartphone(string, bool)      <- 2 arguments
smartphone(string, int, bool) <- 3 arguments

Constructor vs method

Feature / Aspect Constructor Method
Definition A block of code that initialises a newly created object A group of statements that can be called any time to perform a specific task
Naming Same name as the class Must have a different name from the class
Return type None (not even void) Needs a valid return type, or void
Invocation Called implicitly at object creation Called explicitly by the programmer
Default availability If none is written, the C++ compiler provides a default constructor No default method is ever provided

What is a destructor?

A destructor is a special member function that works opposite to a constructor. Constructors initialise an object, destructors destroy it. Its purpose is to free the resources the object acquired during its lifetime.

C++
~class_name() {
    // cleanup code
}

If the object uses new (or its constructor uses new) to allocate heap memory, the destructor should use delete to free that memory.

C++
#include <iostream>
using namespace std;

class Guided_path {
public:
    // Constructor
    Guided_path() {
        cout << "Constructor is called" << endl;
        cout << "Welcome to Guided Path" << endl;
    }

    // Destructor
    ~Guided_path() {
        cout << "Happy Learning" << endl;
        cout << "Destructor is called" << endl;
    }
};

int main() {
    Guided_path obj;   // object created
}                      // object destroyed here
Output:
Output
Constructor is called
Welcome to Guided Path
Happy Learning
Destructor is called
Memoryallocated Constructorruns Objectin use Destructorruns Memoryfreed
Figure: lifetime of an object.

When is a destructor called?

A destructor is called automatically when:

  • the object goes out of scope
  • the program ends
  • a scope ({ }) containing a local object ends
  • the delete operator is called on a heap object
C++
int main() {
    {
        Guided_path a;   // constructed
    }                    // destructor of a runs here

    Guided_path* b = new Guided_path();
    delete b;            // destructor of b runs here
}
Scope ends / delete called → ~ClassName() runs → memory released

Destructor rules

  • The name must begin with a tilde (~) and match the class name.
  • There cannot be more than one destructor in a class (no overloading).
  • A destructor does not accept any parameter.
  • It has no return type, not even void.
  • It should be declared in the public section of the class.
  • The programmer cannot take the address of a destructor.
  • If you do not write one, the compiler generates a default destructor and inserts it.
Constructor Destructor
ClassName() ~ClassName()
Can take parameters Cannot take parameters
Can be overloaded Cannot be overloaded (only one)
Called at object creation Called at object destruction
Allocates / initialises resources Releases resources

Order of constructor and destructor calls

Objects are destroyed in the reverse order of their construction.

C++
class Demo {
    string name;
public:
    Demo(string n) : name(n) { cout << "Create " << name << endl; }
    ~Demo()                  { cout << "Destroy " << name << endl; }
};

int main() {
    Demo a("A");
    Demo b("B");
    Demo c("C");
}
Output:
Output
Create A
Create B
Create C
Destroy C
Destroy B
Destroy A
Construction: A → B → C
Destruction: C → B → A

this Pointer & Copying Mechanisms

What is the this pointer?

The this pointer holds the address of the current object. In simple words, it points to the object on which a member function is currently being called.

C++
#include <bits/stdc++.h>
using namespace std;

class mobile {
    string model;
    int year_of_manufacture;

public:
    void set_details(string model, int year_of_manufacture) {
        this->model = model;
        this->year_of_manufacture = year_of_manufacture;
    }

    void print() {
        cout << this->model << endl;
        cout << this->year_of_manufacture << endl;
    }
};

int main() {
    mobile redmi;
    redmi.set_details("Note 7 Pro", 2019);
    redmi.print();
}
Output:
Output
Note 7 Pro
2019
redmi.set_details("Note 7 Pro", 2019);
   |
   \--> inside the function:  this == &redmi

Uses of the this pointer

  • To refer to the current object's member variables when a parameter has the same name (as above).
  • To pass the current object as a parameter to another function.
  • To return the current object (return *this;) and enable method chaining.
C++
class Counter {
    int n = 0;
public:
    Counter& add(int x) {
        n += x;
        return *this;      // return the current object
    }
    void show() { cout << n << endl; }
};

int main() {
    Counter c;
    c.add(1).add(2).add(3);   // method chaining
    c.show();
}
Output:
Output
6

Shallow copy

A shallow copy creates a new object that is a copy of an existing one but does not allocate new memory for dynamically allocated members. It copies member values directly. If a member is a pointer, the shallow copy copies the address, not the data it points to.

C++
#include <iostream>
using namespace std;

class Shallow {
public:
    int id;        // Non-pointer member (copied by value)
    int* data;     // Pointer member (dynamically allocated memory)

    // Parameterized constructor
    Shallow(int i, int val) {
        id = i;
        data = new int(val);
    }

    // Shallow copy constructor: copies member values directly
    // (Note: The default copy constructor generated by compiler behaves identically)
    Shallow(const Shallow& other) {
        this->id = other.id;       // Value copied: obj1 and obj2 have independent 'id'
        this->data = other.data;   // Address copied: both pointers point to the SAME heap address!
    }

    ~Shallow() {
        delete data; // DANGER: Causes double-free runtime crash when both objects are destroyed!
    }
};

int main() {
    Shallow obj1(101, 42);
    Shallow obj2 = obj1;     // Calls shallow copy constructor

    cout << "--- Initial Values ---" << endl;
    cout << "obj1: id = " << obj1.id << ", *data = " << *(obj1.data) << endl;
    cout << "obj2: id = " << obj2.id << ", *data = " << *(obj2.data) << endl;

    // Modifying obj2's members to see the effect on obj1
    obj2.id = 202;           // Non-pointer: only obj2.id changes (independent copy)
    *obj2.data = 100;        // Pointer: changes shared memory, affects BOTH obj1 and obj2!

    cout << "\n--- After Modifying obj2 (obj2.id = 202, *obj2.data = 100) ---" << endl;
    cout << "obj1: id = " << obj1.id << " (unchanged), *data = " << *(obj1.data) << " (changed!)" << endl;
    cout << "obj2: id = " << obj2.id << " (changed),   *data = " << *(obj2.data) << endl;

    return 0;
}
Output:
Output
--- Initial Values ---
obj1: id = 101, *data = 42
obj2: id = 101, *data = 42

--- After Modifying obj2 (obj2.id = 202, *obj2.data = 100) ---
obj1: id = 101 (unchanged), *data = 100 (changed!)
obj2: id = 202 (changed),   *data = 100

In this example:

  • Non-pointer member (id): Copied by value. Modifying obj2.id does not affect obj1.id because each object has its own separate variable.
  • Pointer member (data): Only the memory address is copied. Both obj1.data and obj2.data point to the same heap memory, so modifying *obj2.data directly alters the value seen by obj1.data.

Double delete (undefined behaviour)

When main() ends, both destructors run delete data; on the same address. Freeing memory twice is undefined behaviour and commonly crashes the program. This is the real danger of shallow copying owning pointers.

Deep copy

A deep copy creates a new object and allocates new memory for dynamically allocated members. It copies the actual data, so the new object has its own copy. Non-pointer members are copied by value, and pointer members allocate brand new memory so that the two objects do not share any addresses.

C++
#include <iostream>
using namespace std;

class Deep {
public:
    int id;        // Non-pointer member (copied by value)
    int* data;     // Pointer member (dynamically allocated memory)

    // Parameterized constructor
    Deep(int i, int val) {
        id = i;
        data = new int(val);
    }

    // Custom copy constructor for deep copy
    Deep(const Deep& other) {
        this->id = other.id;                 // Value copied: obj1 and obj2 have independent 'id'
        this->data = new int(*(other.data)); // New memory allocated + value copied (independent!)
    }

    ~Deep() {
        delete data; // SAFE: each object owns its own separate allocated memory
    }
};

int main() {
    Deep obj1(101, 42);
    Deep obj2 = obj1;        // Calls deep copy constructor

    cout << "--- Initial Values ---" << endl;
    cout << "obj1: id = " << obj1.id << ", *data = " << *(obj1.data) << endl;
    cout << "obj2: id = " << obj2.id << ", *data = " << *(obj2.data) << endl;

    // Modifying obj2's members to verify independence
    obj2.id = 202;           // Non-pointer: only obj2.id changes (independent copy)
    *obj2.data = 100;        // Pointer: modifies obj2's OWN memory; obj1 is UNAFFECTED!

    cout << "\n--- After Modifying obj2 (obj2.id = 202, *obj2.data = 100) ---" << endl;
    cout << "obj1: id = " << obj1.id << " (unchanged), *data = " << *(obj1.data) << " (unchanged!)" << endl;
    cout << "obj2: id = " << obj2.id << " (changed),   *data = " << *(obj2.data) << " (changed!)" << endl;

    return 0;
}
Output:
Output
--- Initial Values ---
obj1: id = 101, *data = 42
obj2: id = 101, *data = 42

--- After Modifying obj2 (obj2.id = 202, *obj2.data = 100) ---
obj1: id = 101 (unchanged), *data = 42 (unchanged!)
obj2: id = 202 (changed),   *data = 100 (changed!)

In this example:

  • Non-pointer member (id): Copied by value into separate memory. Modifying obj2.id does not affect obj1.id.
  • Pointer member (data): Brand new dynamic memory is allocated using new int(*(other.data)). Because obj1.data and obj2.data point to distinct memory locations, modifying *obj2.data has zero effect on *obj1.data.
  • Safe Destruction: When both objects go out of scope, each destructor safely deletes its own separate heap memory, completely avoiding double-free errors.

Deep copy of a C-string member

C++
#include <cstring>

class student {
    int age;
    char* names;

public:
    student(int age, const char* names) {
        this->age = age;

        // deep copy: new array + copy characters
        this->names = new char[strlen(names) + 1];
        strcpy(this->names, names);
    }

    ~student() { delete[] names; }
};

Shallow copy vs deep copy

Shallow copy obj1.data obj2.data int 42(one block) both share the SAME memoryDeep copy obj1.data obj2.data int 42 int 42(new copy) each object owns its OWN memory
Figure: shallow copy duplicates the pointer; deep copy duplicates the data.
Shallow Copy Deep Copy
Stores the references/addresses of objects to the original memory Stores copies of the object's values
Changes made to the copy are reflected in the original Changes made to the copy are not reflected in the original
Copies the object and points the references to the same objects Copies the object and recursively copies the objects it refers to
Faster Comparatively slower
Risk of double delete / dangling pointer Safe ownership: each object frees its own memory
Copying an object Does the class owna raw pointer / resource? Default copy is fine(shallow is safe) Write a deep-copyconstructor + destructor NoYes
Figure: flowchart for choosing between the default (shallow) copy and a custom deep copy.
Rule of Three

If a class needs a custom destructor, it almost always also needs a custom copy constructor and copy assignment operator. In modern C++, prefer smart pointers (std::unique_ptr) or containers (std::vector, std::string) so the compiler-generated versions are correct.

Four Pillars of OOP

The four pillars of OOP

OOP Encapsulationprotect data Abstractionhide implementation Inheritancereuse code Polymorphismmany behaviours
Figure: the four pillars of OOP.
Pillar Keyword Short meaning
Encapsulation Protect data How data is protected
Abstraction Hide implementation What is exposed
Inheritance Reuse code How code is reused
Polymorphism One interface, many behaviours How behaviour changes

1. Encapsulation

Encapsulation is the process of combining data members and member functions into a single unit (class) while restricting direct access to the data. Usually we make data members private and provide public methods to access or modify them.

C++
#include <iostream>
using namespace std;

class BankAccount {
private:
    double balance = 0;

public:
    void deposit(double amount) {
        if (amount > 0) balance += amount;   // validation
    }

    double getBalance() {
        return balance;
    }
};

int main() {
    BankAccount account;
    account.deposit(5000);
    cout << account.getBalance();
}
Output:
Output
5000

balance cannot be accessed directly from outside the class:

C++
account.balance = -5000;   // Error

Instead we use deposit(), which can validate the input.

Why it matters

Encapsulation keeps the object always in a valid state, lets you change the internal implementation later, and reduces bugs caused by outside code modifying data freely.

Data hiding vs encapsulation

Data Hiding Encapsulation
Restricts direct access to data Bundles data and methods together
Mainly concerned with protection Mainly concerned with organisation and controlled access
Uses private, protected Uses classes, methods and access specifiers
Part of encapsulation Broader concept
Remember

Data hiding is a technique; encapsulation is the overall mechanism.

2. Abstraction

Abstraction means exposing only the necessary functionality while hiding the implementation details. In other words: show what an object can do, but hide how it does it.

Real-world example: an ATM

C++
withdraw();
deposit();
checkBalance();

When using an ATM you know these operations, but you do not need to know how the bank internally processes the transaction.

Abstraction in C++

One common way to achieve abstraction in C++ is to use an abstract class together with pure virtual functions.

User  -->  interface (what)  -->  [ hidden implementation (how) ]

Abstract classes

An abstract class is a class that contains at least one pure virtual function. It acts as a base class that defines a common interface while leaving the implementation of specific operations to derived classes.

  • Contains at least one pure virtual function.
  • Objects of an abstract class cannot be instantiated.
  • Derived classes must implement the pure virtual functions.
  • It provides a common interface to achieve abstraction.
C++
#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() = 0;      // pure virtual function
    virtual ~Shape() {}
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing Circle";
    }
};

int main() {
    Shape* s = new Circle();
    s->draw();
    delete s;
}
Output:
Output
Drawing Circle

virtual void draw() = 0; is a pure virtual function. It basically says: "Every Shape must have a draw() function, but I am not going to tell you how draw() works."

Why is Shape an abstract class?

Because Shape contains a pure virtual function, C++ makes it abstract. Therefore you cannot create its object:

C++
Shape s;   // Error
Compiler error:
Error
error: cannot declare variable 's' to be of abstract type 'Shape'
note: because the following virtual functions are pure within 'Shape':
      virtual void Shape::draw()

Why? Because C++ does not know what Shape::draw() should actually do. Should it draw a circle? A rectangle? A triangle? The child class decides.

Child class provides the implementation (override)

C++
class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing Circle";
    }
};
  • draw() : the signature matches the pure virtual function declared in the base class Shape.
  • override : an explicit keyword added in C++11 telling the compiler this function is intended to override a virtual function in the base class.

If the base-class signature changes or does not exist, the compiler gives an error instead of silently creating a new unrelated function.

The user then works with Shape and simply calls:

C++
Shape* s = new Circle();
s->draw();

They do not need to know the internal implementation.

Encapsulation vs abstraction

Encapsulation Abstraction
Focuses on how data is protected Focuses on what is exposed
Hides and protects data Hides implementation details
Achieved using classes and access specifiers Achieved using abstract classes and pure virtual functions
Example: private balance Example: Shape::draw() interface

3. Inheritance

Inheritance allows a class to acquire the data members and member functions of another class.

  • The class being inherited from is the Base / Parent class.
  • The inheriting class is the Derived / Child class.
C++
#include <iostream>
using namespace std;

class Animal {
public:
    void eat() {
        cout << "Eating\n";
    }
};

class Dog : public Animal {
public:
    void bark() {
        cout << "Barking\n";
    }
};

int main() {
    Dog d;
    d.eat();
    d.bark();
}
Output:
Output
Eating
Barking

Dog inherits eat() from Animal, so instead of rewriting eat() in Dog we reuse it.

Animal+ eat() Dog+ bark() Cat+ meow() Cow+ moo()
Figure: hierarchical inheritance. Arrows point from derived class to base class.

Types of inheritance

1. Single inheritance: one base → one derived

C++
class Animal {};
class Dog : public Animal {};

2. Multiple inheritance: one derived → multiple bases

C++
class Camera {};
class Phone {};
class Smartphone : public Camera, public Phone {};

3. Multilevel inheritance: a chain

C++
class Animal {};
class Dog : public Animal {};
class Puppy : public Dog {};

4. Hierarchical inheritance: many derived → one base

C++
class Animal {};
class Dog : public Animal {};
class Cat : public Animal {};
class Cow : public Animal {};

5. Hybrid inheritance: a combination of two or more types

C++
class Animal {};
class Dog : public Animal {};
class Cat : public Animal {};
class Hybrid : public Dog, public Cat {};   // multiple + hierarchical
Type Shape Example
Single A → B Dog : Animal
Multiple A, B → C Smartphone : Camera, Phone
Multilevel A → B → C Animal → Dog → Puppy
Hierarchical A → B, A → C Animal → Dog, Cat, Cow
Hybrid Mix of the above Hybrid : Dog, Cat

Diamond problem

The diamond problem happens when a class inherits from two classes, and both of those classes inherit from the same base class.

      Animal
      /    \
    Dog    Cat
      \    /
      Hybrid
C++
class Animal {
public:
    int age;
};

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

Hybrid receives Animal through two different paths (Hybrid → Dog → Animal and Hybrid → Cat → Animal), so it contains two copies of Animal:

Hybrid
 /    \
Dog   Cat
 |     |
Animal Animal

Why is this a problem?

C++
Hybrid h;
h.age = 10;   // which age?
Compiler error:
Error
error: request for member 'age' is ambiguous

C++ asks: which age are you talking about, the one through Dog or the one through Cat? The access is ambiguous.

C++
h.Dog::age = 10;   // workaround: qualify the path explicitly
h.Cat::age = 20;
Without virtual: TWO Animal copies Hybrid Dog Cat Animal(copy 1) Animal(copy 2) h.age is ambiguousWith virtual: ONE shared Animal Hybrid Dog Cat Animal(one copy) h.age is unambiguous
Figure: the diamond problem and its virtual-inheritance fix.

How does C++ solve it? Virtual inheritance

C++ provides virtual inheritance. Change:

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

to:

C++
class Dog : virtual public Animal {};
class Cat : virtual public Animal {};

Complete example

C++
#include <iostream>
using namespace std;

class Animal {
public:
    int age;
};

class Dog : virtual public Animal {};
class Cat : virtual public Animal {};
class Hybrid : public Dog, public Cat {};

int main() {
    Hybrid h;
    h.age = 10;                 // no ambiguity now
    cout << h.age << endl;
}
Output:
Output
10
Hybrid
 /    \
Dog   Cat
 \    /
 Animal
 (ONE)

Hybrid contains only one shared Animal, so h.age = 10; is no longer ambiguous.

What does `virtual` mean here?

Here virtual does not mean a virtual function or runtime polymorphism. In class Dog : virtual public Animal {}; it means: share a single common Animal base when multiple inheritance paths lead to it.

Summary

The diamond problem occurs in multiple inheritance when a derived class gets the same base class through two different paths. This can create two copies of the base class and ambiguity when accessing its members. C++ solves this with virtual inheritance, where the common base class is shared and only one base-class subobject exists.

4. Polymorphism

Polymorphism means "many forms". It allows the same interface or function to behave differently depending on the situation.

Animal
 |-- Dog -> Bark
 |-- Cat -> Meow
 \-- Cow -> Moo

All of them can have sound(), but each provides different behaviour. There are two main types:

Polymorphism
 |-- Compile-Time (static / early binding)
 |     |-- Function Overloading
 |     \-- Operator Overloading
 |
 \-- Runtime (dynamic / late binding)
       \-- Function Overriding + Virtual Function

Compile-time polymorphism: function overloading

The compiler decides which function to call during compilation. In function overloading, the functions share the same name but have different parameter lists.

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

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

int main() {
    Calculator c;
    cout << c.add(2, 3) << endl;       // int version
    cout << c.add(2.5, 3.5) << endl;   // double version
}
Output:
Output
5
6

The compiler determines the correct function based on the arguments.

Return type alone is not enough

Two functions that differ only in return type cannot be overloaded; the parameter list must differ.

Compile-time polymorphism: operator overloading

Operator overloading lets operators such as +, - and * work with user-defined types. It gives a custom meaning to existing operators and makes objects easier to use.

C++
#include <iostream>
using namespace std;

class Complex {
public:
    int real, imag;

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

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

int main() {
    Complex c1(2, 3);
    Complex c2(4, 5);
    Complex c3 = c1 + c2;

    cout << c3.real << " + " << c3.imag << "i";
}
Output:
Output
6 + 8i

Now the + operator works with our Complex objects.

Runtime polymorphism

The function to execute is determined at runtime. It is achieved using:

  • Inheritance
  • Function overriding
  • virtual functions
  • Base-class pointer / reference
C++
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void sound() {
        cout << "Animal sound\n";
    }
    virtual ~Animal() {}
};

class Dog : public Animal {
public:
    void sound() override {
        cout << "Dog barks\n";
    }
};

class Cat : public Animal {
public:
    void sound() override {
        cout << "Cat meows\n";
    }
};

int main() {
    Animal* a1 = new Dog();
    Animal* a2 = new Cat();

    a1->sound();
    a2->sound();

    delete a1;
    delete a2;
}
Output:
Output
Dog barks
Cat meows

The important part is Animal* a1 = new Dog();: the pointer type is Animal*, but the actual object is Dog. Because sound() is virtual, a1->sound(); calls Dog::sound(). This is runtime polymorphism.

Animal* a(base pointer) Dog object vptr other data Dog vtable &Dog::sound Dog::sound()
Figure: dynamic dispatch. The base pointer reaches the object, the vptr selects the vtable, and the vtable entry calls Dog::sound().

Why virtual is important

Without virtual:

C++
class Animal {
public:
    void sound() { cout << "Animal"; }
};

class Dog : public Animal {
public:
    void sound() { cout << "Dog"; }
};

int main() {
    Animal* a = new Dog();
    a->sound();
}
Output:
Output
Animal

A base pointer calling sound() uses the base-class version instead of dynamically dispatching to the derived one.

With virtual void sound() C++ performs dynamic dispatch, so Dog::sound() runs and prints Dog.

Always use a virtual destructor in a polymorphic base

If you delete a derived object through a base pointer and the base destructor is not virtual, the derived destructor is not called (undefined behaviour). That is why Animal above declares virtual ~Animal() {}.

The override keyword

C++
void sound() override

override tells the compiler: "I intend to override a virtual function from the base class." If the function signature does not actually override a base function, the compiler gives an error.

C++
class Base {
public:
    virtual void show(int x) {}
};

class Derived : public Base {
public:
    void show(double x) override {}   // error: does not override
};
Compiler error:
Error
error: 'void Derived::show(double)' marked 'override', but does not override
Best practice

Always write override on overriding functions. It turns silent signature mistakes into compile errors.

Compile-time vs runtime polymorphism

Compile-Time Runtime
Static polymorphism Dynamic polymorphism
Early binding Late binding
Decided by the compiler Decided during execution
Function overloading, operator overloading Function overriding with virtual functions
Generally faster Has some dispatch overhead (vptr / vtable lookup)
Overloading → resolved at compile time | Overriding + virtual → resolved at run time

Relationships, Advantages & Limitations

Association, aggregation and composition

These three terms describe relationships between classes.

Relationship Meaning Ownership Example
Association uses-a None, independent objects Teacher ↔ Student
Aggregation has-a (weak) Weak, parts can exist independently Company → Employees
Composition has-a (strong) Strong, parts die with the owner House → Rooms
Association  -> uses-a
Aggregation  -> has-a (weak)
Composition  -> has-a (strong)
Aggregation (weak has-a) Company Employee Employee can exist aloneComposition (strong has-a) House Room Room dies with the HouseAssociation (uses-a) Teacher Student
Figure: association, aggregation and composition.

Association

Association is a general relationship where two independent objects interact. It represents a "uses-a" relationship between objects.

Example: Teacher ↔ Student. A teacher can exist without a student, and a student can exist without that teacher.

C++
class Student {
public:
    string name;
};

class Teacher {
public:
    void teach(Student& s) {          // Teacher *uses* Student
        cout << "Teaching " << s.name << endl;
    }
};

int main() {
    Student s;
    s.name = "Asha";
    Teacher t;
    t.teach(s);
}
Output:
Output
Teaching Asha

In C++, association is commonly classified into aggregation (weak association) and composition (strong association).

Aggregation (weak ownership)

Aggregation is a weak has-a relationship. The objects can exist independently.

Example: Company → Employees. If the company is destroyed, the employees can still exist. Company has Employees.

C++
class Employee {
public:
    string name;
    Employee(string n) : name(n) {}
};

class Company {
    vector<Employee*> staff;          // only holds pointers
public:
    void hire(Employee* e) { staff.push_back(e); }
};

int main() {
    Employee e1("Ravi");
    {
        Company c;
        c.hire(&e1);
    }                                  // Company destroyed
    cout << e1.name;                   // Employee still alive
}
Output:
Output
Ravi
Remember

Aggregation = weak ownership. The container does not create or delete the parts.

Composition (strong ownership)

Composition is a strong has-a relationship. The contained object's lifetime depends on the owner.

Example: House → Rooms. Rooms are conceptually part of the house, and destroying the parent object also destroys the child object.

C++
class Room {
public:
    Room()  { cout << "Room created\n"; }
    ~Room() { cout << "Room destroyed\n"; }
};

class House {
    Room room;                         // Room is a member object
public:
    House()  { cout << "House created\n"; }
    ~House() { cout << "House destroyed\n"; }
};

int main() {
    House h;
}
Output:
Output
Room created
House created
House destroyed
Room destroyed
Remember

Composition = strong ownership. The member is constructed before the owner's body and destroyed after it.

Association vs aggregation vs composition

Feature Association Aggregation Composition
Relationship uses-a has-a (weak) has-a (strong)
Lifetime of part Independent Independent of owner Depends on owner
Typical C++ form Reference / pointer parameter Pointer or reference member Member object (by value)
Example Teacher ↔ Student Company → Employees House → Rooms
Association → uses-a | Aggregation → has-a (weak) | Composition → has-a (strong)

Advantages of OOP

Advantage Description
Code reusability Reuse code using classes and inheritance
Organisation Divides the program into logical classes
Modularity Easier testing, debugging and maintenance
DRY Avoids duplicate code (Don't Repeat Yourself)
Scalability Easy to extend large applications
Data security Encapsulation provides controlled data access

Limitations of OOP

Limitation Description
Learning curve OOP concepts can be difficult initially
Design complexity Requires careful class and relationship design
Extra overhead Classes/objects can be unnecessary for small programs
Higher memory usage Objects and virtual functions may consume extra memory
Performance overhead Runtime polymorphism can add slight execution overhead
  • Requires pre-work and proper planning.
  • Not suitable for a very small problem.
  • Proper documentation is required for later use.

Quick interview recall: main features of OOP

Encapsulation -> protect data
Abstraction   -> hide implementation
Inheritance   -> reuse code
Polymorphism  -> one interface, many behaviours
Question Short answer
What is a class? Blueprint / user-defined type; no memory until instantiated
What is an object? An instance of a class; memory is allocated
Constructor vs method? Constructor: same name as class, no return type, implicit call. Method: any name, return type, explicit call
Shallow vs deep copy? Shallow copies pointer values; deep allocates new memory and copies data
Compile-time vs runtime polymorphism? Overloading (early binding) vs overriding with virtual (late binding)
What is an abstract class? A class with at least one pure virtual function; cannot be instantiated
What solves the diamond problem? Virtual inheritance

SOLID Principles in OOP

What is SOLID?

SOLID is a set of five design principles used to write code that is maintainable, flexible, reusable and easy to extend.

Letter Principle Simple meaning
S Single Responsibility Principle One class → one main job
O Open/Closed Principle Extend without modifying existing code
L Liskov Substitution Principle Child should safely replace parent
I Interface Segregation Principle Don't force unnecessary methods
D Dependency Inversion Principle Depend on abstraction, not a concrete class

1. Single Responsibility Principle (SRP)

Meaning: a class should have only one responsibility or one reason to change. In simple words: one class should do one main job.

Bad design

C++
class Invoice {
public:
    void calculateTotal() {
        // calculate invoice
    }
    void printInvoice() {
        // print invoice
    }
    void saveToDatabase() {
        // save invoice
    }
};

Here Invoice is doing three different jobs: calculating, printing and saving to a database. So the class has multiple responsibilities.

Why is that bad?

Because every modification creates a chance of breaking something else. Suppose calculateTotal() is working perfectly, then a developer changes saveToDatabase() because the company moves from MySQL to PostgreSQL. Since everything is inside one large class, the class must be retested, and a change in one responsibility can accidentally affect another.

Important distinction

Changing a class multiple times is normal. Changing the same class for many unrelated reasons is the problem.

Better design

C++
class Invoice {
public:
    void calculateTotal() {
        // calculate invoice
    }
};

class InvoicePrinter {
public:
    void printInvoice() {
        // print invoice
    }
};

class InvoiceRepository {
public:
    void saveToDatabase() {
        // save invoice
    }
};
Class Responsibility
Invoice Calculate invoice
InvoicePrinter Print invoice
InvoiceRepository Save invoice

2. Open/Closed Principle (OCP)

Meaning: a class should be open for extension but closed for modification. We should be able to add new functionality without modifying existing working code.

Bad approach

C++
class Shape {
public:
    string type;
};

class AreaCalculator {
public:
    double calculate(Shape s) {
        if (s.type == "circle") {
            // calculate circle area
        }
        else if (s.type == "rectangle") {
            // calculate rectangle area
        }
        return 0;
    }
};

If we add a triangle we have to modify AreaCalculator. That violates OCP.

Better approach

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

class Circle : public Shape {
public:
    double area() override {
        return 3.14 * 5 * 5;
    }
};

class Rectangle : public Shape {
public:
    double area() override {
        return 10 * 20;
    }
};

Now if we want to add a triangle:

C++
class Triangle : public Shape {
public:
    double area() override {
        return 0.5 * 10 * 5;
    }
};

We extended the system by adding a new class without modifying the existing Shape or other shape classes.

Existing code   -> Don't modify unnecessarily
New requirement -> Add / extend new code
Key idea

OCP is achieved with abstractions + polymorphism (virtual functions).

3. Liskov Substitution Principle (LSP)

Meaning: a derived class should be usable wherever its base class is expected without breaking the program. In simple words: the child should properly behave like its parent.

A Sparrow can replace a Bird:

C++
class Bird {
public:
    virtual void fly() {
        cout << "Flying";
    }
};

class Sparrow : public Bird {
public:
    void fly() override {
        cout << "Sparrow is flying";
    }
};

Bird* bird = new Sparrow();
bird->fly();

This works correctly. But consider:

C++
class Bird {
public:
    virtual void fly() = 0;
};

class Sparrow : public Bird {
public:
    void fly() override {
        cout << "Flying";
    }
};

class Penguin : public Bird {
public:
    void fly() override {
        // Penguin cannot fly
    }
};
LSP violation

Penguin is forced to implement fly() even though penguins cannot fly. This suggests the base abstraction is wrong. Code that expects every Bird to fly would misbehave with a Penguin.

Better design

C++
class Bird {
public:
    virtual void eat() = 0;
    virtual ~Bird() {}
};

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

class Sparrow : public FlyingBird {
public:
    void eat() override { cout << "Eating"; }
    void fly() override { cout << "Flying"; }
};

class Penguin : public Bird {
public:
    void eat() override { cout << "Eating"; }
};
Bird+ eat() FlyingBird+ fly() Penguin Sparrow
Figure: separate FlyingBird from Bird so Penguin never inherits a fly() it cannot honour.
  • Sparrow → Bird + FlyingBird
  • Penguin → Bird only
  • The hierarchy makes more sense, and Penguin is not forced to provide behaviour that does not make sense.

4. Interface Segregation Principle (ISP)

Meaning: a class should not be forced to implement methods that it does not need. In simple words: don't create one huge interface; create smaller, specific interfaces.

C++
class Machine {
public:
    virtual void print() = 0;
    virtual void scan() = 0;
    virtual void fax() = 0;
};

Now imagine a simple printer:

C++
class SimplePrinter : public Machine {
public:
    void print() override {
        cout << "Printing";
    }
    void scan() override {
        // Not required
    }
    void fax() override {
        // Not required
    }
};

The printer is forced to implement scan() and fax() even though it does not need them. This violates ISP.

Better design

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

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

class Fax {
public:
    virtual void fax() = 0;
};

A simple printer implements only what it needs:

C++
class SimplePrinter : public Printer {
public:
    void print() override {
        cout << "Printing";
    }
};

A multifunction machine can implement all three:

C++
class MultiFunctionMachine : public Printer, public Scanner, public Fax {
public:
    void print() override { cout << "Printing"; }
    void scan()  override { cout << "Scanning"; }
    void fax()   override { cout << "Faxing"; }
};
Before:  Machine { print, scan, fax }   <- fat interface
After:   Printer { print }  Scanner { scan }  Fax { fax }  <- small interfaces

5. Dependency Inversion Principle (DIP)

Meaning: high-level modules should depend on abstractions, not on concrete implementations. In simple words: depend on an interface/abstract class, not on a specific class.

Problem

C++
class MySQLDatabase {
public:
    void save() {
        cout << "Saving to MySQL";
    }
};

class UserService {
    MySQLDatabase db;          // tightly coupled
public:
    void saveUser() {
        db.save();
    }
};

Here UserService is directly dependent on MySQLDatabase. If tomorrow we want PostgreSQL, we would need to modify UserService.

Better approach

Create an abstraction:

C++
class Database {
public:
    virtual void save() = 0;
    virtual ~Database() {}
};

Implement it:

C++
class MySQLDatabase : public Database {
public:
    void save() override {
        cout << "Saving to MySQL";
    }
};

class PostgreSQLDatabase : public Database {
public:
    void save() override {
        cout << "Saving to PostgreSQL";
    }
};

Now UserService depends on the abstraction:

C++
class UserService {
    Database* db;
public:
    UserService(Database* database) {
        db = database;
    }
    void saveUser() {
        db->save();
    }
};

We can now use either database:

C++
int main() {
    MySQLDatabase mysql;
    UserService service1(&mysql);
    service1.saveUser();

    PostgreSQLDatabase postgres;
    UserService service2(&postgres);
    service2.saveUser();
}
Output:
Output
Saving to MySQL
Saving to PostgreSQL
Better:   UserService -> Database
                         /      \
                    MySQL    PostgreSQL

Instead of:  UserService -> MySQL

This makes the code loosely coupled and easier to change.

UserService Database(abstraction) MySQLDatabase PostgreSQLDatabase depends on
Figure: Dependency Inversion. UserService depends on the abstraction; concrete databases implement it.
Related idea: dependency injection

Passing the dependency through the constructor (as UserService(Database*) does) is called dependency injection, the usual way to apply DIP.

Easy way to remember SOLID

Principle Simple Meaning
S — Single Responsibility One class → One main job
O — Open/Closed Extend without modifying existing code
L — Liskov Substitution Child should safely replace parent
I — Interface Segregation Don't force unnecessary methods
D — Dependency Inversion Depend on abstraction, not concrete class
Principle Violation smell Typical fix
SRP Class with many unrelated methods Split into focused classes
OCP Growing if/else or switch on type Abstract base + new subclasses
LSP Subclass throws / does nothing for inherited method Redesign the hierarchy
ISP Empty "not required" method bodies Smaller interfaces
DIP new ConcreteClass inside high-level code Inject an abstraction