Object-Oriented Programming binds data and methods in the form of an object and selectively exposes data to other objects. It revolves around the idea of classes and objects, including their definition, instantiation, relationship, communication, and more. In contrast, procedural programming's building block is procedures or functions that perform operations on data.
Why is the idea of class essential in OOP? Here is an analogy to understand: The world around us has much more structure than just numbers and strings. There are cars, people, houses, and all kinds of things that are part of the structure involved in real-world design.
So, if we want to solve real-world problems using software, we need to group values in a meaningful fashion to mimic the real world's structure with their attributes and behaviour. The idea of class helps us define such structures and build scalable and reusable software.
An object is an instance of a class created with specific data, whereas a class is an abstract blueprint used to create more specific, concrete objects. Each object contains data and methods that work on that data.
In the following example, we have defined a Car class with attributes speed and gear, and methods for setting and getting the speed and gear of a car. Inside the main method, we have created two objects of the Car class (car1 and car2) with their own unique speed and gear values.
// Template of the Car Class
class Car {
private:
// Attributes of the car
int speed;
int gear;
public:
// Methods working on the Car attributes
void setSpeed(int s) {
speed = s;
}
void setGear(int g) {
gear = g;
}
int getSpeed() {
return speed;
}
int getGear() {
return gear;
}
};
int main() {
// Creating an object of the class Car
Car car1;
// Calling member methods of the Car Class
car1.setSpeed(50);
car1.setGear(3);
cout << "Speed: " << car1.getSpeed() << endl;
cout << "Gear: " << car1.getGear() << endl;
// Creating another object of the Car class
Car car2;
// Calling member methods of the Car class
car2.setSpeed(60);
car2.setGear(4);
cout << "Speed: " << car2.getSpeed() << endl;
cout << "Gear: " << car2.getGear() << endl;
return 0;
}
On large-scale software projects, it is crucial to take care of the accessibility and security of our data, and encapsulation helps us achieve this goal. Encapsulation is a technique in C++ to hide implementation details of the object from the outside world and only expose a public interface to interact with the object. For this, we use access modifiers: private, protected, and public.
class BankAccount {
private:
double balance;
int accountNumber;
public:
BankAccount(double b, int num) {
balance = b;
accountNumber = num;
}
void deposit(double amount) {
balance = balance + amount;
}
void withdraw(double amount) {
if (amount <= balance) {
balance = balance - amount;
} else {
cout << "Insufficient funds." << endl;
}
}
double getBalance() {
return balance;
}
int getAccountNumber() {
return accountNumber;
}
};
In the above example, the BankAccount class has encapsulated its data by defining balance and accountNumber private. This means that they cannot be accessed or modified by external code, which provides a layer of protection for sensitive information.
The BankAccount class also provides a public interface for interacting with the class objects through public methods: deposit, withdraw, getBalance, and getAccountNumber. These methods are the only ways for external code to access or modify the private variables.
In large-scale software projects that involve millions of lines of code, it is often necessary to reuse existing code or extend functionalities. To accomplish this, we use the concept of inheritance.
Inheritance is a process by which derived classes (subclasses or child classes) inherit attributes and methods of their base class (superclass or parent class). This is also an idea where the base class serves as a template for creating other derived classes.
class Shape {
public:
int width;
int height;
void setWidth(int w) {
width = w;
}
void setHeight(int h) {
height = h;
}
int getWidth() {
return width;
}
int getHeight() {
return height;
}
};
class Rectangle: public Shape {
public:
int getArea() {
return width * height;
}
};
class Square: public Shape {
public:
void setSide(int s) {
width = height = s;
}
int getSide() {
return width;
}
int getArea() {
return width * width;
}
};
int main() {
Rectangle rect;
rect.setWidth(5);
rect.setHeight(7);
cout << "Width: " << rect.getWidth() << endl;
cout << "Height: " << rect.getHeight() << endl;
cout << "Area: " << rect.getArea() << endl;
Square sq;
sq.setSide(5);
cout << "Side: " << sq.getSide() << endl;
cout << "Area: " << sq.getArea() << endl;
return 0;
}
The Shape class serves as the parent class, while Rectangle and Square are child classes that inherit attributes and methods from the Shape class. Child classes also have their own methods, such as the getArea method in the Rectangle class and the getArea, setSide, and getSide methods in the Square class.
In the main function, we have created objects of the child classes and utilized inherited properties from the base class and unique properties defined in the child classes. The Rectangle class inherits the width and height properties from Shape and calculates its area using them. The Square class also inherits these properties but also has its own setSide and getSide methods to set and retrieve its side length.
Abstraction is a process of providing only the essential or relevant information to the user, so the user only needs to know what the code does, not how it does it. For example, to drive a car, one only needs to know the driving process and not the mechanics of the car engine.
Advantages of abstraction:
If we observe, both encapsulation and abstraction work together in the process of designing extensible, modular, and reusable code. But the critical question is: How is encapsulation different from abstraction? The idea is simple: Encapsulation is the process of hiding internal details, and abstraction is the process of exposing relevant details.
class Shape {
public:
virtual double area() = 0; // pure virtual function
};
class Rectangle: public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) {
width = w;
height = h;
}
double area() {
return width * height;
}
};
class Circle: public Shape {
private:
double radius;
public:
Circle(double r) {
radius = r;
}
double area() {
return 3.14159 * radius * radius;
}
};
int main() {
Shape *shape1 = new Rectangle(10, 5);
Shape *shape2 = new Circle(2);
cout << "Area of rectangle: " << shape1->area() << endl;
cout << "Area of circle: " << shape2->area() << endl;
return 0;
}
In this example, Shape class is an abstract class and Rectangle and Circle classes are concrete classes that inherit from Shape class. The area() function is a pure virtual function in Shape class, which means it needs to be implemented in derived classes. Using the object of the shape class, user can use the area() function without knowing how it's implemented.
Polymorphism is the ability of objects of different classes to be treated as if they are objects of a common parent class. This allows the same method name to be used for different purposes in different classes, with the behaviour of the method determined by the class of the object on which it is called.
In simple words, Polymorphism in OOP presents the common interface to perform a single action in different ways and presents the common interface to access similar structures.
There are two types of polymorphism in C++: Compile-time polymorphism (also known as static polymorphism) and Runtime polymorphism (also known as dynamic polymorphism).
We can achieve compile-time polymorphism using method overloading and operator overloading.
In method overloading, multiple methods can have the same name but different parameter lists. The appropriate method call is determined at compile time based on the number and types of arguments passed to it. For example, the parameters list of a function myfun(int a, int b) is (int, float) which is different from the function myfun(float a, int b) parameter list (float, int).
In the following example, we have a class Shape that has two methods with the same name draw, but with different parameters. The first method takes a single string parameter color, and the second method takes two parameters: color and width. When we call the first method, it will print "Drawing a shape with color red" and when we call the second method, it will print "Drawing a shape with color blue and width 10".
class Shape {
public:
void draw(string color) {
cout << "Drawing a shape with color " << color << endl;
}
void draw(string color, int width) {
cout << "Drawing a shape with color " << color << " and width " << width << endl;
}
};
int main() {
Shape s;
s.draw("red");
s.draw("blue", 10);
return 0;
}
Similarly, in operator overloading, operators such as +, -, *, and / can be overloaded to work with user-defined data types. For understanding operator overloading, you can explore this blog: Operator overloading in C++
Runtime polymorphism allows objects of different classes to be used interchangeably. We can achieve it through inheritance and abstract class. Here we define base class as an abstract class and derived classes inherit from it. These derived classes can have their own unique methods and properties, but they can still be treated as objects of the base class.
In other words: In runtime polymorphism, a base class pointer can point to an object of a derived class, and the appropriate function to call is determined at runtime based on the type of object being pointed to. This allows for the dynamic dispatch of functions and for different behaviour to be exhibited by objects of the same class hierarchy.
For example, suppose we have a game where different types of characters can be controlled by the player. Here each character class has its own unique abilities but they all share some common properties.
class Character {
public:
int health;
int attack;
int defense;
virtual void attackEnemy() = 0;
virtual void takeDamage(int damage) = 0;
};
class Warrior: public Character {
public:
void attackEnemy() {
// warrior attack code
}
void takeDamage(int damage) {
// warrior take damage code
}
};
class Mage: public Character {
public:
void attackEnemy() {
// mage attack code
}
void takeDamage(int damage) {
// mage take damage code
}
};
class Thief: public Character {
public:
void attackEnemy() {
// thief attack code
}
void takeDamage(int damage) {
// thief take damage code
}
};
//Inside the main method
Character *playerCharacter = new Warrior();
playerCharacter->attackEnemy();
Character *enemyCharacter = new Thief();
enemyCharacter->takeDamage(10);
Instead of writing separate code to handle each character class, we can create a base class called Character and other classes (Warrior, Mage and Thief) inherit from it. This way, all the characters can be treated as objects of the Character class and we can write code that works with any character without having to know the specific class it belongs to.
This allows for more flexibility and maintainability in the code. For example, if we want to add a new character class or change the behaviour of an existing class, we do not have to change the code that works with characters, because it is not dependent on the specific class.
Enjoy learning, Enjoy OOPS!
Subscribe to get well designed content on data structure and algorithms, machine learning, system design, object orientd programming and math.