Understanding Polymorphism in C++

Polymorphism is a fundamental concept in object-oriented programming, and it plays a key role in achieving code reusability and extensibility in C++. In this tutorial, you will learn what polymorphism is, how it works in C++, and why it's a powerful concept in this language.

What is Polymorphism?

Polymorphism means having many forms. It is formed by combining two Greek words poly, meaning many, and morph, meaning forms. It allows objects of different classes to be treated as objects of a common base class. This means that you can write code that works with multiple derived classes through a common interface, facilitating code organization and simplifying maintenance.

Types of Polymorphism in C++

There are two types of polymorphism in C++:

  1. Compile-time Polymorphism (Static Binding):
  2. The Compile-time Polymorphism in C++ can be achieved through function overloading and operator overloading:

    • Function Overloading: In function overloading, multiple functions have the same name but different parameters. The parameters may be different in terms of number or type.
    • Here's an example with three overloaded add functions:

      #include <iostream>
      
      // Function to add two integers
      int add(int a, int b) {
          return a + b;
      }
      
      // Function to add two doubles
      double add(double a, double b) {
          return a + b;
      }
      
      // Function to concatenate two strings
      std::string add(const std::string& a, const std::string& b) {
          return a + b;
      }
      
      int main() {
          int num1 = 5, num2 = 10;
          double double1 = 2.5, double2 = 3.5;
          std::string str1 = "Hello, ";
          std::string str2 = "world!";
      
          // Calling the overloaded functions
          int sumInt = add(num1, num2);
          double sumDouble = add(double1, double2);
          std::string concatenatedStr = add(str1, str2);
      
          // Displaying the results
          std::cout << "Sum of integers: " << sumInt << std::endl;
          std::cout << "Sum of doubles: " << sumDouble << std::endl;
          std::cout << "Concatenated string: " << concatenatedStr << std::endl;
      
          return 0;
      }
    • Operator Overloading: In operator overloading, operators are redefined without changing its actual meaning.
    • Here's an example:

      #include <iostream>
      using namespace std;
      
      class MyOperator
      {
      private:
          int num;
      
      public:
          MyOperator() : num(5) {}
      
          int operator++()
          {
              num = num + 10;
              return num;
          }
      };
      
      int main()
      {
          MyOperator myOperator;
          int result = ++myOperator; // calling operator++() function"
      
          cout << "Pre-increamenting value on myOperator: " << result;
      
          return 0;
      }

      In this example, we define a class called MyOperator that overloads the pre-increment operator ++ for an integer member variable num. When the operator is applied, it increments num by 10 and returns the updated value. In the main function, we create an instance of the class, and the pre-increment operator is used to increase num by 10, resulting in the value 15. The final value is then printed to the console.

  3. Run-Time Polymorphism (Dynamic Binding):
  4. The Run-time Polymorphism in C++ can be achieved through function overriding using virtual function. Function overriding and virtual functions are closely related concepts in C++ that work together to enable polymorphism. However, they serve different purposes and are used in different contexts:

    • Virtual Function: Virtual functions are declared in a base class with the virtual keyword and are meant to be overridden in derived classes. It enables polymorphism, which allows different derived classes to provide their specific implementations for the same function. In modern C++, you can use the override keyword to indicate that a function in a derived class is intended to override a function in the base class. This helps catch errors during compilation if the function doesn't match the base class method's signature.
    • Here's an example:

      #include <iostream>
      
      // Base class representing a generic Shape
      class Shape {
      
      // Virtual function
      public:
          virtual void draw()
          {
              std::cout << "Drawing a shape" << std::endl;
          }
      };
      
      // Derived class Circle, inheriting from Shape
      class Circle : public Shape
      {
      
          // Override the draw() method for circle
      public:
          void draw() override
          {
              std::cout << "Drawing a circle" << std::endl;
          }
      };
      
      // Derived class Rectangle, inheriting from Shape
      class Rectangle : public Shape
      {
      
          // Override the draw() method for rectangle
      public:
          void draw() override
          {
              std::cout << "Drawing a rectangle" << std::endl;
          }
      };
      
      int main()
      {
          Shape *circle = new Circle();
          Shape *rectangle = new Rectangle();
      
          circle->draw();    // Calls the draw() method of the Circle class
          rectangle->draw(); // Calls the draw() method of the Rectangle class
      
          delete circle;
          delete rectangle;
      
          return 0;
      }

      In this example, a base class Shape defines a virtual method draw() to handle drawing operations. Two derived classes, Circle and Rectangle, inherit from Shape and override the draw() method with their unique drawing instructions. In the main function, we create instances of Circle and Rectangle using base class pointers. When we call the draw() method through these pointers, the appropriate overridden method is executed based on the actual object type. This demonstrates the power of virtual functions, which enable runtime polymorphism and allow different objects to respond differently to the same method call. Proper memory management is maintained through delete to avoid memory leaks.