Object-Oriented Programming (OOPs) in C++

Object-Oriented Programming (OOPs) in C++

Introduction :

In the world of programming, Object-Oriented Programming (OOP) has emerged as a dominant paradigm for designing and building software systems. C++ is one of the most widely used programming languages that fully supports OOP principles, offering developers a powerful toolset to create efficient and modular applications. we will demystify the essential concepts of OOP, guiding you through hands-on examples. Let's dive in and propel your programming prowess to new heights!

Understanding Object-Oriented Programming :

OOP is a programming paradigm that revolves around the concept of objects, which are instances of classes. It prioritizes organizing code into self-contained and reusable modules, fostering modularity, encapsulation, inheritance, and polymorphism. These core principles allow developers to create complex systems by representing real-world entities and their interactions.

Class and objects

  • Class :

    Classes serve as blueprints or templates for creating objects. A class defines the structure and behavior of objects of a particular type. It encapsulates the data (attributes) and operations (methods) that characterize the objects.

    To declare a class in C++, you use the class keyword followed by the class name. Here's an example of a simple class called Person:

      class Person {
          // Class members will be defined here
      };
    

    Inside the class body, you can define various class members, such as attributes and methods, which we'll explore in the following sections.

  • Objects :

    Once you have a class defined, you can create objects or instances of that class. An object represents a specific entity based on the class definition. It has its own set of attributes and can perform actions defined by the class methods.

    To create an object, you declare a variable of the class type. Here's an example of creating two Person objects:

      Person person1;
      Person person2;
    

    In the above code, person1 and person2 are objects of the Person class. Each object has its own memory space to store attribute values and can independently invoke methods defined in the class.

  • Access Modifiers:

    Access modifiers in C++ are keywords that control the visibility and accessibility of class members (attributes and methods) from outside the class. There are three access modifiers in C++:

    1. Public:

      Public is an access modifier that allows class members to be accessed from anywhere, including outside the class.

       class MyClass {
       public:
           int publicAttribute;
      
           void publicMethod() {
               // Code accessible to all
           }
       };
      

      In the above code, both the publicAttribute and publicMethod() are declared as public. They can be accessed and used by any code that has access to an object of the MyClass class.

    2. Private:

      Private is an access modifier that restricts the access of class members to only within the class itself. Private members are not accessible from outside the class, including in derived classes.

       class MyClass {
       private:
           int privateAttribute;
      
           void privateMethod() {
               // Code accessible only within the class
           }
       };
      

      In the above code, both the privateAttribute and privateMethod() are declared as private. They cannot be accessed or used directly from outside the class. Only other member functions within the class have access to private members.

    3. Protected:

      Protected is an access modifier similar to private but with one additional aspect: protected members can be accessed by derived classes. Protected members are not accessible from outside the class but can be accessed by derived classes and their member functions.

       class BaseClass {
       protected:
           int protectedAttribute;
      
           void protectedMethod() {
               // Code accessible within the class and derived classes
           }
       };
      
       class DerivedClass : public BaseClass {
       public:
           void accessProtected() {
               protectedAttribute = 42;  // Accessible in derived class
               protectedMethod();        // Accessible in derived class
           }
       };
      

      In the above code, the protectedAttribute and protectedMethod() are declared as protected in the BaseClass. They are not directly accessible outside the class, but they can be accessed within derived classes like DerivedClass as shown in the accessProtected() method. In the upcoming sections, we will discuss the concepts of base class and derived class in more detail, exploring their relationship and implications.

  • Role of data (attributes) and behavior (methods):

    Attributes in a class represent the data associated with the objects. They define the characteristics or properties of the objects. Methods, on the other hand, define the behavior or actions that objects can perform.

    Let's extend our Person class example with attributes and methods:

      class Person {
          // Attributes
          std::string name;
          int age;
    
      public:
          // Methods
          void setName(const std::string& personName) {
              name = personName;
          }
    
          void setAge(int personAge) {
              age = personAge;
          }
    
          void displayInfo() {
              std::cout << "Name: " << name << std::endl;
              std::cout << "Age: " << age << std::endl;
          }
      };
    

    In the updated Person class, we've added two attributes: name of type std::string and age of type int. These attributes represent the data associated with each Person object.

    Additionally, we've defined three methods:

    1. setName() takes a std::string argument and sets the name attribute of the object.

    2. setAge() takes an int argument and sets the age attribute of the object.

    3. displayInfo() displays the name and age attributes of the object.

To use these attributes and methods, you can access them using the dot (.) operator on an object. Here's an example of using the Person class:

    int main() {
        Person person1;
        person1.setName("John Doe");
        person1.setAge(25);
        person1.displayInfo();

        return 0;
    }

In the above code, we create a Person object named person1. We then use the setName() and setAge() methods to set the attributes of the person1 object. Finally, we call the displayInfo() method to show the stored information.

Output:

    Name: John Doe
    Age: 25
  • Constructor:

    Constructors have the same name as the class and are used to initialize the attributes of an object. They are automatically invoked when an object is created. Constructors can be overloaded, meaning a class can have multiple constructors with different parameter lists.

    1. Default Constructor:

      A default constructor is a constructor that is automatically generated by the compiler when no constructor is defined explicitly in the class. It has no parameters and initializes the attributes of the class with default values. It is used to create objects without providing any initial values.

    2. Parameterized Constructor:

      A parameterized constructor is a constructor that accepts parameters and allows you to initialize the attributes of an object with specific values. It provides flexibility by allowing objects to be created with different initial states.

    3. Copy Constructor:

      A copy constructor is a constructor that creates a new object as a copy of an existing object. It allows you to create a deep copy of the attributes of an object, ensuring that modifications made to one object do not affect the other.

Here's an example of a class with different constructors:

    #include <iostream>
    #include <string>
    class Person {
    public:
        std::string name;
        int age;

        // Default constructor
        Person() {
            name = "blank";
            age = 0;
            std::cout << "Default Constructor called" << std::endl;
        }

        // Parameterized constructor
        Person(const std::string& personName, int personAge) {
            name = personName;
            age = personAge;
            std::cout << "Parameterized Constructor called" << std::endl;
        }

        // Copy constructor
        Person(const Person& other) {
            name = other.name;
            age = other.age;
            std::cout << "Copy Constructor called" << std::endl;
        }  
    };
    int main() {
        // person1 will call Default Constructor
        Person person1;
        std::cout <<"Name: "<<person1.name << std::endl;
        std::cout <<"Age: "<<person1.age << std::endl;

        // person2 will call Parameterized Constructor
        Person person2("Aria Johnson",23);
        std::cout <<"Name: "<<person2.name << std::endl;
        std::cout <<"Age: "<<person2.age << std::endl;

        // person3 will call Copy Constructor
        Person person3(person2);
        std::cout <<"Name: "<<person3.name << std::endl;
        std::cout <<"Age: "<<person3.age << std::endl;
        return 0;
    }

Output:

    Default Constructor called
    Name: blank
    Age: 0
    Parameterized Constructor called
    Name: Aria Johnson
    Age: 23
    Copy Constructor called
    Name: Aria Johnson
    Age: 23
  • Destructor:

    Destructors are invoked automatically when an object goes out of scope or is explicitly destroyed using the delete keyword. Destructors are helpful for releasing any resources allocated by the object, such as memory or file handles.

    Here's an example of a destructor in the Person class:

      #include <iostream>
      class Person {
    
      public:
          Person() {
              std::cout << "Default Constructor executed" << std::endl;
          }
          ~Person() {
              std::cout << "Destructor executed" << std::endl;
          }
      };
      int main() {
          Person p1,p2;
          return 0;
      }
    

    Output:

      Default Constructor executed
      Default Constructor executed
      Destructor executed
      Destructor executed
    
  • Friend Classes and Friend Functions:

    Friend classes and friend functions have access to the private and protected members of a class, even though they are not members of the class itself. They can be useful for granting specific access privileges to external entities.

    1. Friend Classes:

      A friend class is a class that is granted access to the private and protected members of another class. It can access and modify the private and protected attributes of the class it is declared as a friend.

      Here's an example of a friend class:

       class MyClass {
       private:
           int privateData;
      
       public:
           MyClass(int data) : privateData(data) {}
      
           friend class FriendClass;
       };
      
       class FriendClass {
       public:
           void accessPrivateData(MyClass& obj) {
               // Friend class can access private members
               obj.privateData = 42;
           }
       };
      

      In the above code, the FriendClass is declared as a friend of the MyClass. This grants FriendClass access to the private member privateData of MyClass. The accessPrivateData() method of FriendClass demonstrates the ability to modify the private member of MyClass.

    2. Friend Functions:

      A friend function is a non-member function that is granted access to the private and protected members of a class. It can be declared as a friend within the class, allowing it to access and manipulate the class's private members.

      Here's an example of a friend function:

       class MyClass {
       private:
           int privateData;
      
       public:
           MyClass(int data) : privateData(data) {}
      
           friend void friendFunction(MyClass& obj);
       };
      
       void friendFunction(MyClass& obj) {
           // Friend function can access private members
           obj.privateData = 24;
       }
      

      In the above code, the friendFunction is declared as a friend of the MyClass. This grants friendFunction access to the private member privateData of MyClass. The friendFunction can access and modify privateData as if it were a member of the MyClass.

They can be used to grant special privileges to certain entities without compromising encapsulation and data-hiding principles.

Abstraction:

Imagine you're baking a cake. You don't need to know the exact details of how the oven works; you just need to set the temperature and timer. This concept of focusing on what something does rather than how it does. Similarly, in programming, we create abstractions to interact with objects without getting bogged down in every technical detail. Abstraction helps us manage complex systems by simplifying them, making them easier to understand and work with. In C++, we use classes and abstract data types (ADTs) to achieve abstraction, allowing us to show only the necessary parts of an object or system while hiding the complicated stuff.

It simplifies complex systems and allows for easy utilization without the need to understand every detail. Your code can be used by others without them having to know all the inner workings, comparable to driving a car without being a mechanic. And, by modifying the internals, you can future-proof your work without affecting how others use it, similar to upgrading your car without altering the way you drive it.

An important aspect of abstraction is creating meaningful interfaces that match real-world concepts. Think of a TV remote. You have buttons to change channels, adjust volume, and power on/off. You don't need to know the technical details of how it works; you just need to use the buttons to control the TV.

class RemoteControl {
public:
    void changeChannel(int channel) {
        // This is an abstraction of changing TV channels.
    }

    void adjustVolume(int volume) {
        // This is an abstraction of adjusting TV volume.
    }
};

In this example, RemoteControl is an abstraction that provides methods to change channels and adjust the volume. Just like with the TV remote, you don't need to understand how it internally works; you can focus on what it does.

Abstraction is like looking at the world through a telescope. You see the important parts without getting lost in the details. By using classes and ADTs, you can create user-friendly code that's easy to understand and collaborate on. Abstraction is your tool to simplify, organize, and build amazing software.

Encapsulation

Encapsulation is a fundamental concept in OOP that allows us to bundle data (attributes) and the operations (methods or functions) that manipulate that data into a single unit called a class. It is like putting related information and actions into a box.

To understand encapsulation, let's consider a real-world example of a car. A car has various attributes such as its brand, model, color, and current speed. It also has actions it can perform, like accelerating, braking, and changing gears. In OOP, we can represent a car as a class.

Encapsulation helps us achieve two important goals:

  • Data Hiding: Encapsulation hides a class's inner details from the outside. It allows us to specify which attributes and methods are accessible to the outside world and which should remain hidden. This protects the data's integrity by preventing direct manipulation.

    For example, in our car class, we might want to keep the speed attribute private so that it can only be accessed or modified through specific methods. This way, we ensure that the speed is always within a valid range and prevent illegal changes.

  • Abstraction: Encapsulation enables abstract data types (ADTs) that simplify an object's usage by hiding its implementation details. For example, in the car class, users can interact with the car through simple methods without knowing how its engine or gear shifting works. The inner complexities remain hidden, making it easier to use the class.

Here's an example code implementation for the car class:

#include <iostream>
#include <string>

class Car {
private:
    std::string brand;
    std::string model;
    std::string color;
    int speed;

public:
    // Constructor
    Car(const std::string& carBrand, const std::string& carModel, const std::string& carColor)
        : brand(carBrand), model(carModel), color(carColor), speed(0) {}

    // Method to accelerate the car
    void accelerate(int amount) {
        speed += amount;
    }

    // Method to brake and slow down the car
    void brake(int amount) {
        if (speed >= amount) {
            speed -= amount;
        } else {
            speed = 0;
        }
    }

    // Method to get the current speed of the car
    int getSpeed() const {
        return speed;
    }

    // Method to display car information
    void displayInfo() const {
        std::cout << "Brand: " << brand << std::endl;
        std::cout << "Model: " << model << std::endl;
        std::cout << "Color: " << color << std::endl;
        std::cout << "Current Speed: " << speed << " km/h" << std::endl;
    }
};

int main() {
    // Create a car object
    Car myCar("Toyota", "Camry", "Red");

    // Accelerate the car
    myCar.accelerate(50);

    // Display car information
    myCar.displayInfo();

    // Brake and slow down the car
    myCar.brake(20);

    // Display updated car information
    myCar.displayInfo();

    return 0;
}

In the above code, we define the Car class with private attributes such as brand, model, color, and speed. We provide public methods like accelerate, brake, getSpeed, and displayInfo to manipulate and access the car's data.

In the main function, we create an instance of the Car class called myCar with the brand "Toyota", model "Camry", and color "Red". We then call the accelerate method to increase the car's speed by 50 km/h. We display the car's information using the displayInfo method. Next, we call the brake method to reduce the car's speed by 20 km/h. Finally, we display the updated car information again.

This demonstrates how encapsulation allows us to bundle data and related operations within a class, providing a clean and controlled way to interact with objects.

Inheritance :

Inheritance is a core concept in object-oriented programming (OOP) where a new class (called a derived class or subclass) is created based on an existing class (called a base class or superclass). This enables the derived class to inherit attributes and methods from the base class, promoting code reuse and structuring classes in a hierarchical manner.

Think of inheritance like a family tree. You have a parent (base) class, and you create a child (derived) class that inherits traits from the parent while also having its unique traits. This relationship forms a chain, allowing you to build complex structures from simpler ones.

In programming, inheritance simplifies the creation of new classes that share similarities with existing ones, avoiding the need to rewrite the same code. It's like taking a blueprint (base class) and using it to construct variations of a building (derived classes) without starting from scratch each time.

Types of Inheritance in C++:

  1. Single Inheritance:

    Single inheritance involves deriving a new class from a single base class. The derived class inherits the attributes and methods of the base class.

    Syntax:

     class BaseClass {
         // Base class members
     };
    
     class DerivedClass : public BaseClass {
         // Derived class members
     };
    

    Example:

     #include <iostream>
     #include <string>
    
     class Employee {
     protected:
         std::string name;
    
     public:
         Employee(const std::string& empName) : name(empName) {}
    
         std::string getName() {
             return name;
         }
     };
    
     class Manager : public Employee {
     public:
         Manager(const std::string& mgrName) : Employee(mgrName) {}
     };
    
     int main() {
         Manager myManager("John");
         std::cout << "Manager's name: " << myManager.getName() << std::endl;
         // Output: Manager's name: John
    
         return 0;
     }
    

    In the above code, we have created a base class Employee and a derived class Manager that inherits from Employee.

  2. Multiple Inheritance:

    Multiple inheritance allows a class to inherit from multiple base classes. The derived class inherits the attributes and methods from all the base classes.

    Syntax:

     class BaseClass1 {
         // Base class 1 members
     };
    
     class BaseClass2 {
         // Base class 2 members
     };
    
     class DerivedClass : public BaseClass1, public BaseClass2 {
         // Derived class members
     };
    

    Example:

     #include <iostream>
     #include <string>
    
     class Math {
     protected:
         int number;
    
     public:
         Math(int num) : number(num) {}
     };
    
     class String {
     protected:
         std::string text;
    
     public:
         String(const std::string& str) : text(str) {}
     };
    
     class Calculator : public Math, public String {
     public:
         Calculator(int num, const std::string& str) : Math(num), String(str) {}
    
         void display() {
             std::cout << "Number: " << number << ", Text: " << text << std::endl;
         }
     };
    
     int main() {
         Calculator myCalc(42, "Hello");
         myCalc.display(); // Output: Number: 42, Text: Hello
    
         return 0;
     }
    

    In the above code, we have created two base classes Math and String, and a derived class Calculator that inherits from both.

  3. Multilevel Inheritance:

    Multilevel inheritance involves creating a chain of derived classes, where each derived class serves as the base class for the next class.

    Syntax:

     class BaseClass {
         // Base class members
     };
    
     class IntermediateClass : public BaseClass {
         // Intermediate class members
     };
    
     class DerivedClass : public IntermediateClass {
         // Derived class members
     };
    

    Example:

     #include <iostream>
    
     class Appliance {
     protected:
         bool poweredOn;
    
     public:
         Appliance() : poweredOn(false) {}
    
         void powerOn() {
             poweredOn = true;
             std::cout << "Appliance powered on." << std::endl;
         }
    
         void powerOff() {
             poweredOn = false;
             std::cout << "Appliance powered off." << std::endl;
         }
     };
    
     class Electronic : public Appliance {
     protected:
         bool hasDisplay;
    
     public:
         Electronic() : hasDisplay(false) {}
    
         void displayInfo() {
             std::cout << "Electronic appliance with display." << std::endl;
         }
     };
    
     class TV : public Electronic {
     public:
         TV() {
             hasDisplay = true;
             std::cout << "TV created." << std::endl;
             powerOn();  // Power on the TV as part of TV creation.
             displayInfo();
         }
     };
    
     int main() {
         TV myTV; // Output: TV created. Appliance powered on. Electronic appliance with display.
    
         return 0;
     }
    

    Here, we have created a base class Appliance, a derived class Electronic from Appliance, and another derived class TV from Electronic

  4. Hierarchy Inheritance:

    Hierarchy inheritance involves creating multiple derived classes from a single base class. Each derived class inherits the attributes and methods of the base class.

    Syntax:

     class BaseClass {
         // Base class members
     };
    
     class DerivedClass1 : public BaseClass {
         // Derived class 1 members
     };
    
     class DerivedClass2 : public BaseClass {
         // Derived class 2 members
     };
    

    Example:

     #include <iostream>
     #include <string>
    
     class Food {
     protected:
         std::string name;
    
     public:
         Food(const std::string& foodName) : name(foodName) {}
         std::string getName() const {
             return name;
         }
     };
    
     class Fruit : public Food {
     public:
         Fruit(const std::string& fruitName) : Food(fruitName) {}
     };
    
     class Vegetable : public Food {
     public:
         Vegetable(const std::string& vegName) : Food(vegName) {}
     };
    
     int main() {
         Fruit apple("Apple");
         Vegetable carrot("Carrot");
    
         std::cout << "Fruit: " << apple.getName() << std::endl;
         std::cout << "Vegetable: " << carrot.getName() << std::endl;
    
         return 0;
     }
     // Output:
     //   Fruit: Apple
     //   Vegetable: Carrot
    

    Here, we have created a base class Food, and derived classes Fruit and Vegetable from Food.

Polymorphism:

Polymorphism is a fundamental concept in object-oriented programming (OOP) that allows objects of different classes to be treated as instances of a common base class. This enables a high level of flexibility and extensibility in your code.

At its core, polymorphism lets you interact with various objects in a unified manner, regardless of their specific types. This is achieved through inheritance and method overriding. When multiple classes are related through inheritance, they can share certain characteristics and behaviors. Polymorphism builds on this relationship, enabling a single interface to represent multiple concrete implementations.

In C++, there are two main types of polymorphism:

  1. Compile-time Polymorphism: Function Overloading and Operator Overloading

    Compile-time polymorphism, also known as static polymorphism, is achieved through function overloading and operator overloading. Function overloading enables you to define multiple functions with the same name but different parameter lists. The compiler determines the appropriate function to call based on the arguments provided during the function call.

    Example of Function Overloading:

     #include <iostream>
    
     class Calculator {
     public:
         int add(int a, int b) {
             return a + b;
         }
    
         double add(double a, double b) {
             return a + b;
         }
     };
    
     int main() {
         Calculator calc;
         std::cout << calc.add(2, 3) << std::endl;       // Output: 5
         std::cout << calc.add(2.5, 3.7) << std::endl;   // Output: 6.2
    
         return 0;
     }
    

    In this example, the add function is overloaded to accept both integers and doubles as arguments.

    Operator Overloading:

    Operator overloading extends this idea to operators, allowing you to define how operators work for user-defined types. For instance, you can define how the + operator behaves when used with objects of your class

     #include <iostream>
    
     class Vector2D {
     private:
         double x;
         double y;
    
     public:
         Vector2D(double xVal, double yVal) : x(xVal), y(yVal) {}
    
         Vector2D operator+(const Vector2D& other) const {
             return Vector2D(x + other.x, y + other.y);
         }
    
         void display() {
             std::cout << "(" << x << ", " << y << ")" << std::endl;
         }
     };
    
     int main() {
         Vector2D vec1(3.0, 4.0);
         Vector2D vec2(1.0, 2.0);
    
         Vector2D sum = vec1 + vec2;
         sum.display();  // Output: (4, 6)
    
         return 0;
     }
    

    In this example, the Vector2D class represents a two-dimensional vector. The + operator is overloaded to perform vector addition. When the + operator is used with two Vector2D objects, it adds their respective components and returns a new Vector2D object representing the sum.

  2. Runtime Polymorphism: Virtual Functions and Method Overriding

    Runtime polymorphism, also known as dynamic polymorphism, is a core feature of object-oriented programming that enables you to achieve different behaviors at runtime through the use of virtual functions and method overriding. This concept allows you to treat objects of different classes as instances of a common base class, while their specific implementations are determined by their actual types.

    Virtual Functions:

    A virtual function is a function declared in a base class with the virtual keyword. It allows derived classes to provide their own implementations. This enables you to achieve different behaviors based on the actual object type at runtime.

    Method Overriding:

    Method overriding is the process of providing a new implementation for a virtual function in a derived class. To override a virtual function, the function signature in the derived class should match the base class function's signature exactly, including the function name, return type, and parameter list.

    By overriding a virtual function, you enable the derived class to customize the behavior of that function while still adhering to the base class interface.

    Here's an example demonstrating virtual functions and method overriding:

     #include <iostream>
    
     class Shape {
     public:
         virtual void draw() {
             std::cout << "Drawing a shape." << std::endl;
         }
     };
    
     class Circle : public Shape {
     public:
         void draw() override {
             std::cout << "Drawing a circle." << std::endl;
         }
     };
    
     class Square : public Shape {
     public:
         void draw() override {
             std::cout << "Drawing a square." << std::endl;
         }
     };
    
     int main() {
         Shape* shapes[2];
         Circle circle;
         Square square;
    
         shapes[0] = &circle;
         shapes[1] = &square;
    
         for (int i = 0; i < 2; ++i) {
             shapes[i]->draw();
         }
    
         return 0;
     }
    

    The Shape class defines a virtual function draw().The Circle and Square classes override the draw() function.In the main() function, an array of Shape pointers is created, containing instances of Circle and Square.The draw() function is called on each object through the base class pointer, resulting in the correct derived class behavior being invoked based on the actual object type.

    Output:

     Drawing a circle.
     Drawing a square.
    

Conclusion:

As we conclude our in-depth exploration of Object-Oriented Programming (OOP) in C++, take a moment to reflect on the ground we've covered. We've delved into the core pillars of OOP, from classes and objects to inheritance, encapsulation, and polymorphism.

Every concept we've explored contributes to writing cleaner, more efficient, and maintainable code. We've equipped ourselves with the tools to design robust software that mirrors real-world interactions.

If you feel confident about these concepts or have questions, I encourage you to reach out. Let's continue to learn and grow together, ensuring that you're well-prepared to apply these OOP principles effectively in your future projects.

I trust that the reading proved to be beneficial for you in some way. I appreciate you taking the time to peruse it.

Thank You.