Object-Oriented Programming in C++
Classes & Objects, Access Specifiers, Constructors & Destructors, the this Pointer, Shallow & Deep Copy, the Four Pillars of OOP and SOLID Principles
Table of Contents
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
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
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 |
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)
class_name objectName;
Creating an object dynamically (heap)
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.
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;
}
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 | 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 |
#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;
}
Model : iphone 11
Year of Manufacture : 2019
5g Supported : 0
Model : redmi note 11 t
Year of Manufacture : 2021
5g Supported : 1
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
#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;
}
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++) |
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:
publicprivateprotected
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
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?
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
}
error: 'int smartphone::password' is private within this context
error: 'void smartphone::about_phone()' is protected within this context
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.
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.
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.
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.
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.
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");
Instead of assigning inside the body, you can initialise members directly:
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 (&).
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.
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
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);orA 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.
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.
~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.
#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
Constructor is called
Welcome to Guided Path
Happy Learning
Destructor is called
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
deleteoperator is called on a heap object
int main() {
{
Guided_path a; // constructed
} // destructor of a runs here
Guided_path* b = new Guided_path();
delete b; // destructor of b runs here
}
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.
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");
}
Create A
Create B
Create C
Destroy C
Destroy B
Destroy 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.
#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();
}
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.
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();
}
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.
#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;
}
--- 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. Modifyingobj2.iddoes not affectobj1.idbecause each object has its own separate variable. - Pointer member (
data): Only the memory address is copied. Bothobj1.dataandobj2.datapoint to the same heap memory, so modifying*obj2.datadirectly alters the value seen byobj1.data.
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.
#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;
}
--- 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. Modifyingobj2.iddoes not affectobj1.id. - Pointer member (
data): Brand new dynamic memory is allocated usingnew int(*(other.data)). Becauseobj1.dataandobj2.datapoint to distinct memory locations, modifying*obj2.datahas 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
#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 | 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 |
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
| 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.
#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();
}
5000
balance cannot be accessed directly from outside the class:
account.balance = -5000; // Error
Instead we use deposit(), which can validate the input.
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 |
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
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.
#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;
}
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:
Shape s; // 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)
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing Circle";
}
};
draw(): the signature matches the pure virtual function declared in the base classShape.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:
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.
#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();
}
Eating
Barking
Dog inherits eat() from Animal, so instead of rewriting
eat() in Dog we reuse it.
Types of inheritance
1. Single inheritance: one base → one derived
class Animal {};
class Dog : public Animal {};
2. Multiple inheritance: one derived → multiple bases
class Camera {};
class Phone {};
class Smartphone : public Camera, public Phone {};
3. Multilevel inheritance: a chain
class Animal {};
class Dog : public Animal {};
class Puppy : public Dog {};
4. Hierarchical inheritance: many derived → one base
class Animal {};
class Dog : public Animal {};
class Cat : public Animal {};
class Cow : public Animal {};
5. Hybrid inheritance: a combination of two or more types
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
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?
Hybrid h;
h.age = 10; // which age?
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.
h.Dog::age = 10; // workaround: qualify the path explicitly
h.Cat::age = 20;
How does C++ solve it? Virtual inheritance
C++ provides virtual inheritance. Change:
class Dog : public Animal {};
class Cat : public Animal {};
to:
class Dog : virtual public Animal {};
class Cat : virtual public Animal {};
Complete example
#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;
}
10
Hybrid / \ Dog Cat \ / Animal (ONE)
Hybrid contains only one shared Animal, so
h.age = 10; is no longer ambiguous.
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.
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.
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
}
5
6
The compiler determines the correct function based on the arguments.
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.
#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";
}
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
virtualfunctions- Base-class pointer / reference
#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;
}
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.
Why virtual is important
Without virtual:
class Animal {
public:
void sound() { cout << "Animal"; }
};
class Dog : public Animal {
public:
void sound() { cout << "Dog"; }
};
int main() {
Animal* a = new Dog();
a->sound();
}
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.
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
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.
class Base {
public:
virtual void show(int x) {}
};
class Derived : public Base {
public:
void show(double x) override {} // error: does not override
};
error: 'void Derived::show(double)' marked 'override', but does not override
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) |
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)
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.
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);
}
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.
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
}
Ravi
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.
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;
}
Room created
House created
House destroyed
Room destroyed
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 |
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
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.
Changing a class multiple times is normal. Changing the same class for many unrelated reasons is the problem.
Better design
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
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
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:
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
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:
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:
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
}
};
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
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"; }
};
Sparrow→Bird+FlyingBirdPenguin→Birdonly- The hierarchy makes more sense, and
Penguinis 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.
class Machine {
public:
virtual void print() = 0;
virtual void scan() = 0;
virtual void fax() = 0;
};
Now imagine a simple printer:
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
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:
class SimplePrinter : public Printer {
public:
void print() override {
cout << "Printing";
}
};
A multifunction machine can implement all three:
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
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:
class Database {
public:
virtual void save() = 0;
virtual ~Database() {}
};
Implement it:
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:
class UserService {
Database* db;
public:
UserService(Database* database) {
db = database;
}
void saveUser() {
db->save();
}
};
We can now use either database:
int main() {
MySQLDatabase mysql;
UserService service1(&mysql);
service1.saveUser();
PostgreSQLDatabase postgres;
UserService service2(&postgres);
service2.saveUser();
}
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.
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 |