Unveiling the Power of C++: A Journey Through Object-Oriented Programming

Unveiling the Power of C++: A Journey Through Object-Oriented Programming

From Classes to Exceptions, Inheritance to Polymorphism - A Journey Through C++ OOP Concepts

Object-Oriented Programming, commonly known as OOP, is a fundamental paradigm in the world of software development. It offers a powerful way to model real-world entities, their properties, and their interactions within a software application. In this blog, we will explore the key concepts of OOP in C++, one of the most widely used programming languages for this paradigm.

Chapter 1: Introduction

Object-Oriented Programming is a programming paradigm that revolves around the concept of objects. Objects are instances of classes, and classes are templates or blueprints for creating objects. OOP promotes the following principles:

  1. Abstraction: It allows you to model real-world entities as objects, abstracting away unnecessary details.

  2. Encapsulation: You can hide the internal state of an object, making it accessible only through well-defined interfaces.

  3. Inheritance: Classes can inherit properties and behaviors from other classes, promoting code reusability.

  4. Polymorphism: Objects of different classes can be treated as objects of a common base class, enabling flexibility in handling different object types.

Chapter 2: Objects and Classes

In Chapter 2, we will take a closer look at objects and classes in C++, understanding how they are defined, instantiated, and used in practice. Objects are the central building blocks of Object-Oriented Programming (OOP) in C++, and classes serve as the blueprints for creating these objects.

Objects

An object in C++ is a concrete instance of a class. It represents a specific entity or concept in your program. Objects have both data (attributes) and behaviors (methods) associated with them. Here's an example to illustrate objects:

#include <iostream>
using namespace std;

class Car {
public:
    string make;
    string model;
    int year;

    void start() {
        cout << "Engine started." << endl;
    }

    void stop() {
        cout << "Engine stopped." << endl;
    }
};

int main() {
    // Creating objects of the Car class
    Car car1;
    car1.make = "Toyota";
    car1.model = "Camry";
    car1.year = 2020;

    Car car2;
    car2.make = "Ford";
    car2.model = "Mustang";
    car2.year = 2022;

    // Using object methods
    car1.start();
    car2.stop();

    return 0;
}

In this example, we have a Car class with attributes make, model, and year, and two objects of the Car class (car1 and car2). We set the attributes for each object and call the start and stop methods, demonstrating how objects encapsulate data and behavior.

Classes

A class is a blueprint or template for creating objects. It defines the structure and behavior of objects that will be instantiated from it. Here's how you define a class in C++:

class ClassName {
public:
    // Member variables (attributes)
    data_type variable_name;

    // Member functions (methods)
    return_type method_name(parameters) {
        // Method implementation
    }
};

Let's create a simple class and object to illustrate the concept:

#include <iostream>
using namespace std;

class Circle {
public:
    double radius;

    double calculateArea() {
        return 3.14159265359 * radius * radius;
    }
};

int main() {
    // Creating an object of the Circle class
    Circle myCircle;
    myCircle.radius = 5.0;

    // Using the object's method
    double area = myCircle.calculateArea();
    cout << "Area of the circle: " << area << endl;

    return 0;
}

In this example, we have a Circle class with a radius attribute and a calculateArea method. We create an object myCircle and set its radius. We then call the calculateArea method on myCircle to compute and display the area of the circle.

Classes are the foundation of C++'s OOP paradigm, allowing you to create organized and reusable code by encapsulating data and behaviors within well-defined structures. As you delve deeper into OOP, you'll discover more advanced features like constructors, destructors, inheritance, and polymorphism, which enhance the power and flexibility of classes and objects in C++.

Chapter 3: Class Methods

Chapter 3 dives into class methods, also known as member functions. These methods are an integral part of a class in C++ and are responsible for defining the behaviors and operations associated with objects of that class.

Member Functions

In C++, a class method, or member function, is a function that is declared within a class and operates on the data members of that class. Member functions encapsulate the logic that defines how objects of the class should behave. Let's explore this concept with a practical example:

#include <iostream>
using namespace std;

class Rectangle {
public:
    double length;
    double width;

    // Member function to calculate the area of the rectangle
    double calculateArea() {
        return length * width;
    }

    // Member function to calculate the perimeter of the rectangle
    double calculatePerimeter() {
        return 2 * (length + width);
    }
};

int main() {
    Rectangle rect;
    rect.length = 5.0;
    rect.width = 3.0;

    double area = rect.calculateArea();
    double perimeter = rect.calculatePerimeter();

    cout << "Area: " << area << endl;
    cout << "Perimeter: " << perimeter << endl;

    return 0;
}

In this example, we have a Rectangle class with two data members, length and width. It also includes two member functions: calculateArea() and calculatePerimeter(). These functions perform calculations based on the data members. In the main() function, we create an instance of the Rectangle class and call its member functions to calculate the area and perimeter of a rectangle.

Member Functions vs. Static Functions

It's important to note that C++ also allows the creation of static functions, which are not tied to a specific instance of the class. Member functions, on the other hand, operate on the data of an object and are called using object instances. Here's an example to illustrate the difference:

#include <iostream>
using namespace std;

class MathUtility {
public:
    // Member function to calculate the square of a number
    double square(double x) {
        return x * x;
    }

    // Static function to calculate the cube of a number
    static double cube(double x) {
        return x * x * x;
    }
};

int main() {
    MathUtility math;

    // Calling a member function
    double squareResult = math.square(4.0);

    // Calling a static function
    double cubeResult = MathUtility::cube(3.0);

    cout << "Square: " << squareResult << endl;
    cout << "Cube: " << cubeResult << endl;

    return 0;
}

In this example, the square() function is a member function, while the cube() function is a static function. You can see that the member function is called using an instance of the class (math) and can access the data members (if any). In contrast, the static function is called using the class name itself (MathUtility::cube()) and does not have access to instance-specific data.

Understanding when to use member functions and static functions is crucial for designing classes in C++. Member functions are typically used when you need to operate on instance-specific data, while static functions are suitable for operations that don't rely on object state.

Chapter 4: Constructors and Destructors

In this chapter, we'll explore the concepts of constructors and destructors in C++. Constructors are special member functions that are called when an object is created, allowing you to initialize its state. Destructors, on the other hand, are called when an object is destroyed and are used for cleanup.

Constructors

Default Constructor

A default constructor is a constructor that is automatically generated if you don't provide any constructors in your class. It initializes the object's data members to their default values.

#include <iostream>
using namespace std;

class Rectangle {
public:
    double length;
    double width;

    // Default constructor
    Rectangle() {
        length = 0.0;
        width = 0.0;
    }

    double calculateArea() {
        return length * width;
    }
};

int main() {
    Rectangle rect; // Calls the default constructor
    cout << "Default Rectangle: " << rect.length << " x " << rect.width << endl;
    return 0;
}

In this example, we have a Rectangle class with a default constructor. When you create an instance of Rectangle without providing any initial values, the default constructor is called, and the length and width are set to 0.0.

Parameterized Constructor

You can also define custom constructors that accept parameters, allowing you to initialize the object with specific values.

#include <iostream>
using namespace std;

class Rectangle {
public:
    double length;
    double width;

    // Parameterized constructor
    Rectangle(double l, double w) {
        length = l;
        width = w;
    }

    double calculateArea() {
        return length * width;
    }
};

int main() {
    Rectangle rect(5.0, 3.0); // Calls the parameterized constructor
    cout << "Rectangle: " << rect.length << " x " << rect.width << endl;
    return 0;
}

In this example, the Rectangle class has a parameterized constructor that takes length and width as arguments. When you create an instance of Rectangle and provide values, the parameterized constructor is called to initialize the object.

Destructors

Destructors are used to clean up resources and perform any necessary cleanup when an object is destroyed. They have the same name as the class, preceded by a tilde (~).

#include <iostream>
using namespace std;

class Resource {
public:
    Resource() {
        cout << "Resource created" << endl;
    }

    ~Resource() {
        cout << "Resource destroyed" << endl;
    }
};

int main() {
    Resource* resPtr = new Resource;
    delete resPtr; // Calls the destructor
    return 0;
}

In this example, we have a Resource class with a constructor that prints a message when the resource is created and a destructor that prints a message when the resource is destroyed. When you create an instance of Resource using new and later delete it, the destructor is called automatically to release resources.

Constructors and destructors are essential for proper resource management and initialization in C++. They ensure that objects are in a valid state when created and that any allocated resources are properly released when objects are destroyed, helping prevent memory leaks and resource leaks.

Chapter 5: Access Specifiers

In this chapter, we will delve into access specifiers in C++. Access specifiers define the visibility and accessibility of class members (data members and member functions). C++ supports three primary access specifiers: public, private, and protected.

Public Access Specifier

Members declared under the public access specifier are accessible from anywhere in the program, whether inside or outside the class. This is the least restrictive access specifier.

#include <iostream>
using namespace std;

class MyClass {
public:
    int publicVar;

    void publicMethod() {
        cout << "This is a public method." << endl;
    }
};

int main() {
    MyClass obj;
    obj.publicVar = 42;
    obj.publicMethod();

    cout << "Accessing publicVar from outside the class: " << obj.publicVar << endl;
    return 0;
}

In this example, publicVar and publicMethod() are declared under the public access specifier, making them accessible from both inside and outside the class. This allows you to manipulate publicVar and call publicMethod() from the main() function.

Private Access Specifier

Members declared under the private access specifier are only accessible within the class where they are defined. They are not accessible from outside the class.

#include <iostream>
using namespace std;

class MyClass {
private:
    int privateVar;

    void privateMethod() {
        cout << "This is a private method." << endl;
    }

public:
    void accessPrivate() {
        privateVar = 42;
        privateMethod();
    }
};

int main() {
    MyClass obj;
    obj.accessPrivate();  // Calls accessPrivate() method

    // The following lines would result in compilation errors
    // obj.privateVar = 42;  // Error: 'privateVar' is private
    // obj.privateMethod();  // Error: 'privateMethod' is private

    return 0;
}

In this example, privateVar and privateMethod() are declared under the private access specifier, making them only accessible within the class MyClass. The main() function cannot directly access or modify these private members, but it can use a public member function, accessPrivate(), to indirectly work with them.

Protected Access Specifier

Members declared under the protected access specifier are similar to private members but can be accessed by derived classes. This specifier is commonly used in inheritance to allow derived classes to inherit and modify base class members.

#include <iostream>
using namespace std;

class BaseClass {
protected:
    int protectedVar;

    void protectedMethod() {
        cout << "This is a protected method." << endl;
    }
};

class DerivedClass : public BaseClass {
public:
    void accessProtected() {
        protectedVar = 42;
        protectedMethod();
    }
};

int main() {
    DerivedClass obj;
    obj.accessProtected();  // Calls accessProtected() method

    // The following lines would result in compilation errors
    // obj.protectedVar = 42;  // Error: 'protectedVar' is protected
    // obj.protectedMethod();  // Error: 'protectedMethod' is protected

    return 0;
}

In this example, protectedVar and protectedMethod() are declared under the protected access specifier in the BaseClass. Although these members are not directly accessible from outside the class, they can be accessed within the DerivedClass, which inherits from BaseClass.

Access specifiers are crucial for controlling the visibility and encapsulation of class members in C++. Properly using access specifiers helps ensure data integrity and promotes the principle of encapsulation in object-oriented programming.

Chapter 6: Encapsulation

In this chapter, we will explore the concept of encapsulation in C++. Encapsulation is one of the fundamental principles of Object-Oriented Programming (OOP). It involves bundling data (attributes) and methods (member functions) that operate on that data into a single unit, the class. This bundling ensures that the data is accessed and modified in a controlled manner, often using access specifiers.

Encapsulation in C++

Private Data Members

To achieve encapsulation in C++, data members (attributes) are typically marked as private. This means that they are not directly accessible from outside the class. Access to these private members is controlled through public member functions.

#include <iostream>
using namespace std;

class Student {
private:
    string name;
    int age;

public:
    // Setter method for the 'name' attribute
    void setName(string studentName) {
        name = studentName;
    }

    // Getter method for the 'name' attribute
    string getName() {
        return name;
    }

    // Setter method for the 'age' attribute
    void setAge(int studentAge) {
        if (studentAge > 0) {
            age = studentAge;
        }
    }

    // Getter method for the 'age' attribute
    int getAge() {
        return age;
    }
};

int main() {
    Student student1;

    // Accessing and modifying data through public member functions
    student1.setName("Alice");
    student1.setAge(20);

    cout << "Name: " << student1.getName() << endl;
    cout << "Age: " << student1.getAge() << endl;

    return 0;
}

In this example, the name and age data members of the Student class are marked as private. Access to these attributes is controlled through public setter and getter methods, setName, getName, setAge, and getAge. This encapsulates the data and ensures that it can be accessed and modified in a controlled manner, allowing validation and data integrity checks.

Advantages of Encapsulation

  1. Data Integrity: Encapsulation helps maintain data integrity by controlling how data can be accessed and modified. It enables you to apply validation and constraints to ensure the data's consistency.

  2. Flexibility: It allows you to change the internal representation of data without affecting the code that uses the class. For example, you can change the name attribute from a string to a char array without affecting the code that uses the Student class.

  3. Code Organization: Encapsulation promotes a clean and organized structure. By bundling data and methods together, you create a more logical and maintainable codebase.

  4. Security: Encapsulation restricts direct access to sensitive data, enhancing security.

  5. Reusability: Encapsulated classes and objects can be reused in different parts of the program or even in other projects, enhancing code reusability.

Encapsulation is a fundamental principle that underlies the concept of access specifiers and the design of classes in C++. By practicing encapsulation, you can create more robust, maintainable, and secure software systems.

Chapter 7: Inheritance

Inheritance is a fundamental concept in Object-Oriented Programming (OOP). It allows you to create new classes that are based on existing classes, inheriting their properties and behaviors. In this chapter, we'll explore inheritance in C++ in-depth, along with practical examples.

The Basics of Inheritance

In C++, you can create a new class (derived or child class) based on an existing class (base or parent class). The derived class inherits the attributes and methods of the base class, and you can extend, modify, or override them as needed.

Base Class

Let's start with a simple example of a base class representing a Person:

#include <iostream>
using namespace std;

class Person {
public:
    string name;
    int age;

    void display() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

Derived Class

Now, let's create a derived class Student that inherits from the Person class:

class Student : public Person {
public:
    int studentId;

    void study() {
        cout << name << " is studying." << endl;
    }
};

Here, we use the : public Person syntax to specify that Student is a derived class that inherits publicly from the Person base class. This means that all the public attributes and methods of Person are accessible within the Student class.

Using Inheritance

Now, let's demonstrate how to use the Person and Student classes:

int main() {
    Person person;
    person.name = "John";
    person.age = 30;
    person.display();

    Student student;
    student.name = "Alice";
    student.age = 20;
    student.studentId = 12345;
    student.display();
    student.study();

    return 0;
}

In this example, we create instances of both the Person and Student classes. The Person class's attributes and methods are inherited by the Student class. As a result, you can set the name and age attributes and call the display method for both Person and Student objects. Additionally, the Student class has its specific attribute, studentId, and a method, study.

Method Overriding

Inheritance allows you to override base class methods in the derived class. This is known as method overriding. When a method in the derived class has the same name and signature as a method in the base class, it overrides the base class method.

Here's an example of method overriding:

class Teacher : public Person {
public:
    void display() {
        cout << "Teacher: " << name << ", Age: " << age << endl;
    }
};

In this example, the Teacher class inherits from Person and overrides the display method. When you create a Teacher object and call the display method, the overridden version in the Teacher class is executed.

Inheritance is a powerful mechanism in OOP that promotes code reuse and modeling relationships between objects. It allows you to create a hierarchy of classes, each building upon the previous one, while still retaining the ability to customize and extend behavior as needed. This concept is vital for creating well-structured and efficient software systems.

Chapter 8: Polymorphism

Polymorphism is one of the core principles of Object-Oriented Programming (OOP). It allows objects of different classes to be treated as objects of a common base class. In C++, polymorphism is achieved through virtual functions and interfaces, enabling flexibility in handling objects of various types. In this chapter, we'll explore polymorphism in depth with practical examples.

The Basics of Polymorphism

Polymorphism allows you to work with objects of different classes in a uniform way. It relies on the following key concepts:

  1. Base Class: Define a base class with one or more virtual functions. A virtual function is a function declared in the base class and marked with the virtual keyword. This function can be overridden in derived classes.

  2. Derived Classes: Create derived classes that inherit from the base class and override the virtual function(s) as needed.

  3. Pointers and References: Use pointers or references to the base class to interact with objects of derived classes. This is where polymorphism comes into play.

Example: Virtual Functions

Let's start with an example using virtual functions. Consider a base class Shape with a virtual function area():

#include <iostream>
using namespace std;

class Shape {
public:
    virtual double area() {
        return 0.0;
    }
};

Now, create two derived classes, Circle and Rectangle, and override the area() function in each class:

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    double area() override {
        return 3.14159265359 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double length;
    double width;

public:
    Rectangle(double l, double w) : length(l), width(w) {}

    double area() override {
        return length * width;
    }
};

In the main function, you can create objects of the derived classes and access their area() methods through pointers to the base class:

int main() {
    Shape* shape1 = new Circle(5.0);
    Shape* shape2 = new Rectangle(4.0, 3.0);

    cout << "Area of shape1: " << shape1->area() << endl;
    cout << "Area of shape2: " << shape2->area() << endl;

    delete shape1;
    delete shape2;

    return 0;
}

In this example, you create Circle and Rectangle objects but store them in pointers to the base class Shape. By doing this, you can use polymorphism to call the appropriate area() method, depending on the actual type of the object, even though you're accessing it through a base class pointer.

Benefits of Polymorphism

Polymorphism promotes code reusability and flexibility in handling different object types without the need for extensive conditional logic. It simplifies code maintenance and supports the dynamic behavior of objects in a complex system. Polymorphism is a powerful tool in C++ and is widely used in object-oriented software design and development.

Chapter 9: Files

Working with files is an integral part of many software applications. In C++, you can read from and write to files using various classes provided by the Standard Template Library (STL). In this chapter, we'll explore how to perform file operations, including reading and writing, in C++ with practical examples.

Reading from Files

To read from a file in C++, you typically use the ifstream (input file stream) class. Here's an example of how to read from a text file:

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main() {
    ifstream inputFile("sample.txt");
    if (!inputFile) {
        cerr << "Error opening file." << endl;
        return 1;
    }

    string line;
    while (getline(inputFile, line)) {
        cout << line << endl;
    }

    inputFile.close();

    return 0;
}

In this example, we use an ifstream to open a file named "sample.txt" for reading. If the file is opened successfully, we use a while loop to read lines from the file and display them. The close() method is called to close the file after reading.

Writing to Files

To write to a file in C++, you typically use the ofstream (output file stream) class. Here's an example of how to write to a text file:

#include <iostream>
#include <fstream>
using namespace std;

int main() {
    ofstream outputFile("output.txt");
    if (!outputFile) {
        cerr << "Error opening file." << endl;
        return 1;
    }

    outputFile << "Hello, world!" << endl;
    outputFile << "This is a sample text file." << endl;

    outputFile.close();

    return 0;
}

In this example, we use an ofstream to create a new file named "output.txt" for writing. If the file is created successfully, we use the << operator to write text to the file. The close() method is called to close the file after writing.

Appending to Files

To append data to an existing file, you can use the ios::app flag when opening the file:

#include <iostream>
#include <fstream>
using namespace std;

int main() {
    ofstream outputFile("output.txt", ios::app);
    if (!outputFile) {
        cerr << "Error opening file." << endl;
        return 1;
    }

    outputFile << "Appending some data." << endl;

    outputFile.close();

    return 0;
}

In this example, we open the "output.txt" file with the ios::app flag to append data to the end of the existing file.

Binary File Operations

In addition to text files, C++ allows you to work with binary files. To work with binary files, you can use the fstream class and specify the ios::binary flag when opening the file. Binary files are suitable for storing non-textual data, such as images or binary data.

#include <iostream>
#include <fstream>
using namespace std;

int main() {
    fstream binaryFile("data.dat", ios::binary | ios::out);
    if (!binaryFile) {
        cerr << "Error opening binary file." << endl;
        return 1;
    }

    int data[] = {1, 2, 3, 4, 5};
    binaryFile.write(reinterpret_cast<char*>(data), sizeof(data));

    binaryFile.close();

    return 0;
}

In this example, we create a binary file "data.dat" and write an array of integers to it using the write() method.

Working with files in C++ is essential for various applications, from text file processing to handling binary data. The Standard Template Library (STL) provides robust file handling classes, making it convenient to perform file operations in C++.

Chapter 10: Exceptions

Handling exceptions is a crucial aspect of C++ programming that allows you to gracefully manage and recover from unexpected errors and exceptional conditions. In this chapter, we'll explore the concept of exceptions in C++ and learn how to use them effectively with practical examples.

The Basics of Exception Handling

In C++, exceptions are used to deal with runtime errors that may occur during program execution. An exception is an object representing an error or an exceptional condition. Exception handling allows you to detect and respond to such conditions, rather than letting them cause the program to crash.

Throwing Exceptions

To throw an exception, you use the throw keyword followed by an object that represents the exception. Commonly, exceptions are objects of classes derived from the std::exception class.

Here's an example of throwing an exception:

#include <iostream>
#include <stdexcept>
using namespace std;

int divide(int a, int b) {
    if (b == 0) {
        throw runtime_error("Division by zero");
    }
    return a / b;
}

int main() {
    try {
        int result = divide(10, 0);
        cout << "Result: " << result << endl;
    } catch (const exception& e) {
        cerr << "Exception caught: " << e.what() << endl;
    }
    return 0;
}

In this example, the divide function throws an exception of type std::runtime_error when an attempt is made to divide by zero. In the main function, we use a try block to enclose the code that might throw an exception. If an exception is thrown, it's caught by the catch block, and an error message is printed.

Catching Exceptions

In the catch block, you can catch exceptions by their type or by using a base class such as std::exception. Catching exceptions by type allows you to handle different exceptions differently. Here's an example:

try {
    // Code that may throw exceptions
} catch (const std::runtime_error& e) {
    // Handle runtime error
} catch (const std::logic_error& e) {
    // Handle logic error
} catch (const std::exception& e) {
    // Handle other exceptions
}

Rethrowing Exceptions

You can rethrow an exception using the throw statement within a catch block. This allows you to catch an exception, perform some processing, and then rethrow the same or a different exception.

try {
    // Code that may throw exceptions
} catch (const std::exception& e) {
    // Handle the exception
    throw; // Rethrow the same exception
}

Custom Exceptions

You can create your own custom exception classes by deriving from std::exception or its subclasses. This allows you to define specific exceptions tailored to your program's needs.

#include <iostream>
#include <stdexcept>
using namespace std;

class MyException : public exception {
public:
    const char* what() const noexcept {
        return "Custom exception occurred";
    }
};

int main() {
    try {
        throw MyException();
    } catch (const exception& e) {
        cerr << "Exception caught: " << e.what() << endl;
    }
    return 0;
}

In this example, we define a custom exception class MyException that inherits from std::exception. We override the what() method to provide a custom error message.

Exception handling is a critical part of C++ programming, allowing you to handle errors and exceptional conditions in a structured and robust way. Proper use of exceptions can improve the reliability and maintainability of your code, making it more resilient to unexpected situations.

Conclusion

In this comprehensive guide, we've delved into the world of Object-Oriented Programming (OOP) in C++, breaking down its key components and principles. From the foundational concepts of objects and classes to more advanced topics like inheritance and polymorphism, we've explored how C++ empowers developers to build organized, maintainable, and efficient software.

By harnessing the power of abstraction, encapsulation, inheritance, and polymorphism, C++ developers can create elegant solutions that closely model real-world scenarios. This approach not only enhances code reusability but also simplifies complex systems by breaking them down into manageable, interconnected objects.

Understanding the role of constructors and destructors ensures proper resource management, while access specifiers help maintain data integrity by controlling access to class members. The file handling capabilities of C++ make it a versatile language for performing operations on external data sources. Additionally, the built-in exception handling mechanisms provide a robust way to manage errors and ensure that your applications remain stable and user-friendly.

As you continue your journey into C++ and OOP, remember that mastery comes with practice and hands-on experience. OOP is more than just a programming paradigm; it's a powerful mindset that can transform how you approach software development. When used effectively, OOP principles can lead to more efficient, maintainable, and scalable codebases.

By applying the knowledge and skills you've gained from this guide, you can embark on the path to becoming a proficient C++ developer, capable of creating elegant and powerful software solutions that stand the test of time. So, dive into your coding projects, embrace the principles of OOP, and continue to explore the vast possibilities of C++ development.

Did you find this article valuable?

Support Mayank Aggarwal by becoming a sponsor. Any amount is appreciated!