C++ is a powerful programming language that supports the features of object oriented programming. It is widely used in game development, embedded systems, high-performance computing, etc. On the other side, many popular C++ libraries and frameworks are developed using the idea of OOPS concepts.
In this blog, we will explore brief highlights of fundamental OOPS concepts in C++: Classes and Objects, Encapsulation, Abstraction, Inheritance and Polymorphism.
A class is a fundamental building block of OOPS in C++. It is a user-defined data type that encapsulates data and methods into a single unit and serves as a blueprint for creating objects. On the other side, an object is an instance of a class created with specific data. So each object contains data and methods that work on that data.
The critical question is: Why is the idea of class and object important in OOPS? Here is an analogy to understand: The world around us has a much more complex structure than just numbers or strings. There are cars, people, houses, and all kinds of things that are part of the real-world design. So if we want to develop real-life software applications, we need to group attributes, behaviour and relationship of real-world objects in a meaningful fashion. So, the idea of class and object helps us define such structures in real-life software.
Using classes and objects:
In C++, we define a class using the class keyword followed by the name of the class.
class ClassName {
// Member variables
// Member functions
};
In the following example, we have defined a Car class with attributes speed and gear and provided methods for accessing and updating these attributes. Inside the main method, we have created two objects of the Car (car1 and car2) with their own speed and gear values.
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;
}
Now, the critical question is: What is the meaning of "private" and "public" in the above code? Why do we use methods like getSpeed() and setSpeed() to access and modify data members? To know the answer, we need to understand the idea of encapsulation.
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 hides the implementation details of the object from the outside world and only exposes a public interface to interact with the object. So it will help us to achieve abstraction because users of the object do not need to know the implementation details to use the object.
To achieve encapsulation, we use access modifiers: private, protected, and public. The private member can only be accessed within the class, while the public members can be accessed from anywhere. The protected member is similar to private, but its scope is only limited to the same class or derived classes.
class BankAccount {
private:
double balance;
int accountNumber;
// Private method
void printTransaction(double amount, const string& action) {
cout << "Transaction: " << action << " $" << amount << endl;
}
public:
BankAccount(double b, int num) {
balance = b;
accountNumber = num;
}
void deposit(double amount) {
balance = balance + amount;
printTransaction(amount, "Deposit");
}
void withdraw(double amount) {
if (amount <= balance) {
balance = balance - amount;
printTransaction(amount, "Withdrawal");
} else {
cout << "Insufficient funds." << endl;
}
}
double getBalance() {
return balance;
}
int getAccountNumber() {
return accountNumber;
}
};
int main() {
// Create a BankAccount object
BankAccount account(1000.0, 123456);
// Perform operations on the account
account.deposit(500.0);
account.withdraw(200.0);
// Display account information
cout << "Account Number: " << account.getAccountNumber() << endl;
cout << "Balance: $" << account.getBalance() << endl;
return 0;
}
Overall, encapsulation provides flexibility in code development because the implementation details of an object can be changed without affecting the code of any other objects that use it.
In large-scale software projects that involve millions of lines of code, it is often necessary to reuse existing code or extend existing functionalities. To accomplish this, we use the concept of inheritance.
Inheritance helps us create new classes based on existing classes. It is a process by which derived classes (subclasses or child classes) inherit attributes and methods of their base class (superclass or parent class).
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;
}
In the above code, Shape is the parent class, while Rectangle and Square are child classes. These two classes inherit attributes and methods from the Shape class. Child classes also have their own methods: getArea() method in the Rectangle class and getArea(), setSide(), and getSide() methods in the Square class.
In the main function, we have created objects of the child classes and used inherited properties from the base class. 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.
Polymorphism presents the common interface to perform a single action in different ways. There are two types of polymorphism in C++: Compile-time polymorphism (static binding) and Runtime polymorphism (dynamic binding).
In C++, 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.
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 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. We can achieve it using inheritance and abstract class.
What is an abstract class? In C++, an abstract class contains at least one pure virtual function and it cannot be instantiated directly. We use it to provide a common blueprint for its derived classes by specifying a set of virtual functions that must be implemented by the derived classes.
What is a pure virtual function? In C++, a pure virtual function is declared by appending = 0 to the function declaration in a base class but it has no implementation. It must be overridden by derived classes.
Now come to the main idea of runtime polymorphism: We define the abstract base class (which contains pure virtual functions) and derived classes inherit from it. Derived class provide the concrete implementation of the virtual functions defined in the base class.
Now we can assign any object of a derived class with the base class pointer. When we call a virtual function using a base class pointer, the appropriate implementation of the virtual function is determined at runtime based on the type of object being pointed to. So this idea will enable different derived classes to have their own implementations of the same function, which can be selected dynamically based on the type of object being referred to.
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 an abstract base class called Character and other classes of specific characters (Warrior, Mage and Thief) inherit from it. This way, all the characters can be treated as objects of the Character class. On the other side, by using an abstract base class, we enforce any derived class representing a character to provide concrete implementations for the pure virtual functions.
So in this way, different types of characters can have their own unique behaviours and they can be treated uniformly through the abstract base class interface. If we want to add a new character class or change the existing behaviour in a class, we do not need to change the code that works with characters, because it is not dependent on the specific class.
This is a good example of abstraction using the idea of polymorphism and inheritance.
Abstraction is a process of providing only the essential details 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 engine. We can easily replace the existing engine with a new engine, without affecting the driving process.
If we observe, both encapsulation and abstraction work together. 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:
// pure virtual function
virtual double area() = 0;
};
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 Recta ngle(10, 5);
Shape *shape2 = new Circle(2);
cout << "Area of rectangle: " << shape1->area() << endl;
cout << "Area of circle: " << shape2->area() << endl;
return 0;
}
Shape class: It is an abstract base class that defines the common interface for all shapes. It declares a pure virtual function area(), so any derived class must provide an implementation for calculating the area.
Creating derived classes: Rectangle and Circle classes are derived from the Shape class and they provide their own implementation of the area() method.
Using polymorphism: Inside the main, we used polymorphism to create two shape objects: shape1 (object of Rectangle class) and shape2 (object of Circle class). Since Shape is an abstract class, it cannot be instantiated directly. So, we have created pointers to the base class Shape and assigned derived class objects to them.
This will help us call specific implementations of the area() using the Shape class pointer, without being concerned about implementation details in derived classes. This is an idea of abstraction: How the areas are calculated is abstracted away from the client code.
In the coming future, we will write separate detailed blogs on several concepts mentioned in this blog. If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, oops!